Merge Tusky 14

This commit is contained in:
Mike Barnes 2021-03-02 09:25:38 +11:00
commit 76782ca4b3
210 changed files with 6117 additions and 2249 deletions

View file

@ -20,8 +20,8 @@ android {
applicationId APP_ID applicationId APP_ID
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 29
versionCode 79 versionCode 80
versionName "13.1-CW1" versionName "14.0-CW1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
@ -68,6 +68,9 @@ android {
androidExtensions { androidExtensions {
experimental = true experimental = true
} }
buildFeatures {
viewBinding true
}
testOptions { testOptions {
unitTests { unitTests {
returnDefaultValues = true returnDefaultValues = true

View file

@ -43,6 +43,10 @@
public *; public *;
} }
-keep enum com.keylesspalace.tusky.db.DraftAttachment$Type {
public *;
}
# preserve line numbers for crash reporting # preserve line numbers for crash reporting
-keepattributes SourceFile,LineNumberTable -keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute SourceFile -renamesourcefileattribute SourceFile

View file

@ -0,0 +1,747 @@
{
"formatVersion": 1,
"database": {
"version": 24,
"identityHash": "ea8559bbdf434c7b9086384a9a4cc8e6",
"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, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` 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": "notificationsFollowRequested",
"columnName": "notificationsFollowRequested",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsReblogged",
"columnName": "notificationsReblogged",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsFavorited",
"columnName": "notificationsFavorited",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsPolls",
"columnName": "notificationsPolls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsSubscriptions",
"columnName": "notificationsSubscriptions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationSound",
"columnName": "notificationSound",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationVibration",
"columnName": "notificationVibration",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationLight",
"columnName": "notificationLight",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "defaultPostPrivacy",
"columnName": "defaultPostPrivacy",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "defaultMediaSensitivity",
"columnName": "defaultMediaSensitivity",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "alwaysShowSensitiveMedia",
"columnName": "alwaysShowSensitiveMedia",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "alwaysOpenSpoiler",
"columnName": "alwaysOpenSpoiler",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mediaPreviewEnabled",
"columnName": "mediaPreviewEnabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastNotificationId",
"columnName": "lastNotificationId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activeNotifications",
"columnName": "activeNotifications",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "emojis",
"columnName": "emojis",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tabPreferences",
"columnName": "tabPreferences",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notificationsFilter",
"columnName": "notificationsFilter",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_AccountEntity_domain_accountId",
"unique": true,
"columnNames": [
"domain",
"accountId"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)"
}
],
"foreignKeys": []
},
{
"tableName": "InstanceEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))",
"fields": [
{
"fieldPath": "instance",
"columnName": "instance",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "emojiList",
"columnName": "emojiList",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "maximumTootCharacters",
"columnName": "maximumTootCharacters",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxPollOptions",
"columnName": "maxPollOptions",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxPollOptionLength",
"columnName": "maxPollOptionLength",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "version",
"columnName": "version",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"instance"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TimelineStatusEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "timelineUserId",
"columnName": "timelineUserId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "authorServerId",
"columnName": "authorServerId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inReplyToId",
"columnName": "inReplyToId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inReplyToAccountId",
"columnName": "inReplyToAccountId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "createdAt",
"columnName": "createdAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "emojis",
"columnName": "emojis",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reblogsCount",
"columnName": "reblogsCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favouritesCount",
"columnName": "favouritesCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "reblogged",
"columnName": "reblogged",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bookmarked",
"columnName": "bookmarked",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favourited",
"columnName": "favourited",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sensitive",
"columnName": "sensitive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "spoilerText",
"columnName": "spoilerText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "visibility",
"columnName": "visibility",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "attachments",
"columnName": "attachments",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "mentions",
"columnName": "mentions",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "application",
"columnName": "application",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reblogServerId",
"columnName": "reblogServerId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reblogAccountId",
"columnName": "reblogAccountId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "poll",
"columnName": "poll",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "muted",
"columnName": "muted",
"affinity": "INTEGER",
"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, 'ea8559bbdf434c7b9086384a9a4cc8e6')"
]
}
}

View file

@ -0,0 +1,821 @@
{
"formatVersion": 1,
"database": {
"version": 25,
"identityHash": "e2cb844862443c2c5cc884c11f120d43",
"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": "DraftEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "inReplyToId",
"columnName": "inReplyToId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "contentWarning",
"columnName": "contentWarning",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sensitive",
"columnName": "sensitive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "visibility",
"columnName": "visibility",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "attachments",
"columnName": "attachments",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "poll",
"columnName": "poll",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "failedToSend",
"columnName": "failedToSend",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "AccountEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` 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": "notificationsFollowRequested",
"columnName": "notificationsFollowRequested",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsReblogged",
"columnName": "notificationsReblogged",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsFavorited",
"columnName": "notificationsFavorited",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsPolls",
"columnName": "notificationsPolls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsSubscriptions",
"columnName": "notificationsSubscriptions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationSound",
"columnName": "notificationSound",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationVibration",
"columnName": "notificationVibration",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationLight",
"columnName": "notificationLight",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "defaultPostPrivacy",
"columnName": "defaultPostPrivacy",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "defaultMediaSensitivity",
"columnName": "defaultMediaSensitivity",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "alwaysShowSensitiveMedia",
"columnName": "alwaysShowSensitiveMedia",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "alwaysOpenSpoiler",
"columnName": "alwaysOpenSpoiler",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mediaPreviewEnabled",
"columnName": "mediaPreviewEnabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastNotificationId",
"columnName": "lastNotificationId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activeNotifications",
"columnName": "activeNotifications",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "emojis",
"columnName": "emojis",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tabPreferences",
"columnName": "tabPreferences",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notificationsFilter",
"columnName": "notificationsFilter",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_AccountEntity_domain_accountId",
"unique": true,
"columnNames": [
"domain",
"accountId"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)"
}
],
"foreignKeys": []
},
{
"tableName": "InstanceEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))",
"fields": [
{
"fieldPath": "instance",
"columnName": "instance",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "emojiList",
"columnName": "emojiList",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "maximumTootCharacters",
"columnName": "maximumTootCharacters",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxPollOptions",
"columnName": "maxPollOptions",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxPollOptionLength",
"columnName": "maxPollOptionLength",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "version",
"columnName": "version",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"instance"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TimelineStatusEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "timelineUserId",
"columnName": "timelineUserId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "authorServerId",
"columnName": "authorServerId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inReplyToId",
"columnName": "inReplyToId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inReplyToAccountId",
"columnName": "inReplyToAccountId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "createdAt",
"columnName": "createdAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "emojis",
"columnName": "emojis",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reblogsCount",
"columnName": "reblogsCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favouritesCount",
"columnName": "favouritesCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "reblogged",
"columnName": "reblogged",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bookmarked",
"columnName": "bookmarked",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favourited",
"columnName": "favourited",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sensitive",
"columnName": "sensitive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "spoilerText",
"columnName": "spoilerText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "visibility",
"columnName": "visibility",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "attachments",
"columnName": "attachments",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "mentions",
"columnName": "mentions",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "application",
"columnName": "application",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reblogServerId",
"columnName": "reblogServerId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reblogAccountId",
"columnName": "reblogAccountId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "poll",
"columnName": "poll",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "muted",
"columnName": "muted",
"affinity": "INTEGER",
"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, 'e2cb844862443c2c5cc884c11f120d43')"
]
}
}

View file

@ -146,6 +146,7 @@
<activity android:name=".components.instancemute.InstanceListActivity" /> <activity android:name=".components.instancemute.InstanceListActivity" />
<activity android:name=".components.scheduled.ScheduledTootActivity" /> <activity android:name=".components.scheduled.ScheduledTootActivity" />
<activity android:name=".components.announcements.AnnouncementsActivity" /> <activity android:name=".components.announcements.AnnouncementsActivity" />
<activity android:name=".components.drafts.DraftsActivity" />
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" /> <receiver android:name=".receiver.NotificationClearBroadcastReceiver" />
<receiver <receiver

View file

@ -8,7 +8,6 @@ import android.text.SpannableStringBuilder
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.text.style.URLSpan import android.text.style.URLSpan
import android.text.util.Linkify import android.text.util.Linkify
import android.view.MenuItem
import android.widget.TextView import android.widget.TextView
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.util.CustomURLSpan import com.keylesspalace.tusky.util.CustomURLSpan
@ -50,16 +49,6 @@ class AboutActivity : BottomSheetActivity(), Injectable {
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
return true
}
}
return super.onOptionsItemSelected(item)
}
} }
private fun TextView.setClickableTextWithoutUnderlines(@StringRes textId: Int) { private fun TextView.setClickableTextWithoutUnderlines(@StringRes textId: Int) {

View file

@ -57,6 +57,7 @@ import com.keylesspalace.tusky.interfaces.ActionButtonActivity
import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.interfaces.ReselectableFragment
import com.keylesspalace.tusky.pager.AccountPagerAdapter import com.keylesspalace.tusky.pager.AccountPagerAdapter
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.view.showMuteAccountDialog import com.keylesspalace.tusky.view.showMuteAccountDialog
import com.keylesspalace.tusky.viewmodel.AccountViewModel import com.keylesspalace.tusky.viewmodel.AccountViewModel
@ -77,16 +78,18 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
private val viewModel: AccountViewModel by viewModels { viewModelFactory } private val viewModel: AccountViewModel by viewModels { viewModelFactory }
private val accountFieldAdapter = AccountFieldAdapter(this) private lateinit var accountFieldAdapter : AccountFieldAdapter
private var followState: FollowState = FollowState.NOT_FOLLOWING private var followState: FollowState = FollowState.NOT_FOLLOWING
private var blocking: Boolean = false private var blocking: Boolean = false
private var muting: Boolean = false private var muting: Boolean = false
private var blockingDomain: Boolean = false private var blockingDomain: Boolean = false
private var showingReblogs: Boolean = false private var showingReblogs: Boolean = false
private var subscribing: Boolean = false
private var loadedAccount: Account? = null private var loadedAccount: Account? = null
private var animateAvatar: Boolean = false private var animateAvatar: Boolean = false
private var animateEmojis: Boolean = false
// fields for scroll animation // fields for scroll animation
private var hideFab: Boolean = false private var hideFab: Boolean = false
@ -116,12 +119,13 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
loadResources() loadResources()
makeNotificationBarTransparent() makeNotificationBarTransparent()
setContentView(R.layout.activity_account) setContentView(R.layout.activity_account)
// Obtain information to fill out the profile. // Obtain information to fill out the profile.
viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID)!!) viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID)!!)
val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this) val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)
animateAvatar = sharedPrefs.getBoolean("animateGifAvatars", false) animateAvatar = sharedPrefs.getBoolean("animateGifAvatars", false)
animateEmojis = sharedPrefs.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
hideFab = sharedPrefs.getBoolean("fabHide", false) hideFab = sharedPrefs.getBoolean("fabHide", false)
setupToolbar() setupToolbar()
@ -159,8 +163,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
accountMuteButton.hide() accountMuteButton.hide()
accountFollowsYouTextView.hide() accountFollowsYouTextView.hide()
// setup the RecyclerView for the account fields // setup the RecyclerView for the account fields
accountFieldAdapter = AccountFieldAdapter(this, animateEmojis)
accountFieldList.isNestedScrollingEnabled = false accountFieldList.isNestedScrollingEnabled = false
accountFieldList.layoutManager = LinearLayoutManager(this) accountFieldList.layoutManager = LinearLayoutManager(this)
accountFieldList.adapter = accountFieldAdapter accountFieldList.adapter = accountFieldAdapter
@ -186,6 +190,16 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
accountTabLayout.postDelayed({ poorTabView.isPressed = false }, 300) accountTabLayout.postDelayed({ poorTabView.isPressed = false }, 300)
} }
// If wellbeing mode is enabled, follow stats and posts count should be hidden
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_PROFILE, false)
if (wellbeingEnabled) {
accountStatuses.hide()
accountFollowers.hide()
accountFollowing.hide()
}
} }
/** /**
@ -200,8 +214,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
val pageTitles = arrayOf(getString(R.string.title_statuses), getString(R.string.title_statuses_with_replies), getString(R.string.title_statuses_pinned), getString(R.string.title_media)) val pageTitles = arrayOf(getString(R.string.title_statuses), getString(R.string.title_statuses_with_replies), getString(R.string.title_statuses_pinned), getString(R.string.title_media))
TabLayoutMediator(accountTabLayout, accountFragmentViewPager) { TabLayoutMediator(accountTabLayout, accountFragmentViewPager) { tab, position ->
tab, position ->
tab.text = pageTitles[position] tab.text = pageTitles[position]
}.attach() }.attach()
@ -365,16 +378,15 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
val usernameFormatted = getString(R.string.status_username_format, account.username) val usernameFormatted = getString(R.string.status_username_format, account.username)
accountUsernameTextView.text = usernameFormatted accountUsernameTextView.text = usernameFormatted
accountDisplayNameTextView.text = account.name.emojify(account.emojis, accountDisplayNameTextView) accountDisplayNameTextView.text = account.name.emojify(account.emojis, accountDisplayNameTextView, animateEmojis)
val emojifiedNote = account.note.emojify(account.emojis, accountNoteTextView) val emojifiedNote = account.note.emojify(account.emojis, accountNoteTextView, animateEmojis)
LinkHelper.setClickableText(accountNoteTextView, emojifiedNote, null, this) LinkHelper.setClickableText(accountNoteTextView, emojifiedNote, null, this)
// accountFieldAdapter.fields = account.fields ?: emptyList() // accountFieldAdapter.fields = account.fields ?: emptyList()
accountFieldAdapter.emojis = account.emojis ?: emptyList() accountFieldAdapter.emojis = account.emojis ?: emptyList()
accountFieldAdapter.notifyDataSetChanged() accountFieldAdapter.notifyDataSetChanged()
accountLockedImageView.visible(account.locked) accountLockedImageView.visible(account.locked)
accountBadgeTextView.visible(account.bot) accountBadgeTextView.visible(account.bot)
@ -428,7 +440,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
private fun updateToolbar() { private fun updateToolbar() {
loadedAccount?.let { account -> loadedAccount?.let { account ->
val emojifiedName = account.name.emojify(account.emojis, accountToolbar) val emojifiedName = account.name.emojify(account.emojis, accountToolbar, animateEmojis)
try { try {
supportActionBar?.title = EmojiCompat.get().process(emojifiedName) supportActionBar?.title = EmojiCompat.get().process(emojifiedName)
@ -536,13 +548,32 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
blockingDomain = relation.blockingDomain blockingDomain = relation.blockingDomain
showingReblogs = relation.showingReblogs showingReblogs = relation.showingReblogs
accountFollowsYouTextView.visible(relation.followedBy) // If wellbeing mode is enabled, "follows you" text should not be visible
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_PROFILE, false)
accountFollowsYouTextView.visible(relation.followedBy && !wellbeingEnabled)
// because subscribing is Pleroma extension, enable it __only__ when we have non-null subscribing field
// it's also now supported in Mastodon 3.3.0rc but called notifying and use different API call
if(!viewModel.isSelf && followState == FollowState.FOLLOWING
&& (relation.subscribing != null || relation.notifying != null)) {
accountSubscribeButton.show()
accountSubscribeButton.setOnClickListener {
viewModel.changeSubscribingState()
}
if(relation.notifying != null)
subscribing = relation.notifying
else if(relation.subscribing != null)
subscribing = relation.subscribing
}
// remove the listener so it doesn't fire on non-user changes
accountNoteTextInputLayout.editText?.removeTextChangedListener(noteWatcher)
accountNoteTextInputLayout.visible(relation.note != null) accountNoteTextInputLayout.visible(relation.note != null)
accountNoteTextInputLayout.editText?.setText(relation.note) accountNoteTextInputLayout.editText?.setText(relation.note)
// add the listener late to avoid it firing on the first change
accountNoteTextInputLayout.editText?.removeTextChangedListener(noteWatcher)
accountNoteTextInputLayout.editText?.addTextChangedListener(noteWatcher) accountNoteTextInputLayout.editText?.addTextChangedListener(noteWatcher)
updateButtons() updateButtons()
@ -574,6 +605,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
accountFollowButton.setText(R.string.action_unfollow) accountFollowButton.setText(R.string.action_unfollow)
} }
} }
updateSubscribeButton()
} }
private fun updateMuteButton() { private fun updateMuteButton() {
@ -584,6 +616,20 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
} }
} }
private fun updateSubscribeButton() {
if(followState != FollowState.FOLLOWING) {
accountSubscribeButton.hide()
}
if(subscribing) {
accountSubscribeButton.setIconResource(R.drawable.ic_notifications_active_24dp)
accountSubscribeButton.contentDescription = getString(R.string.action_unsubscribe_account)
} else {
accountSubscribeButton.setIconResource(R.drawable.ic_notifications_24dp)
accountSubscribeButton.contentDescription = getString(R.string.action_subscribe_account)
}
}
private fun updateButtons() { private fun updateButtons() {
invalidateOptionsMenu() invalidateOptionsMenu()
@ -595,6 +641,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
if (blocking || viewModel.isSelf) { if (blocking || viewModel.isSelf) {
accountFloatingActionButton.hide() accountFloatingActionButton.hide()
accountMuteButton.hide() accountMuteButton.hide()
accountSubscribeButton.hide()
} else { } else {
accountFloatingActionButton.show() accountFloatingActionButton.show()
if (muting) if (muting)
@ -608,6 +655,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
accountFloatingActionButton.hide() accountFloatingActionButton.hide()
accountFollowButton.hide() accountFollowButton.hide()
accountMuteButton.hide() accountMuteButton.hide()
accountSubscribeButton.hide()
} }
} }
@ -615,14 +663,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
menuInflater.inflate(R.menu.account_toolbar, menu) menuInflater.inflate(R.menu.account_toolbar, menu)
if (!viewModel.isSelf) { if (!viewModel.isSelf) {
val follow = menu.findItem(R.id.action_follow)
follow.title = if (followState == FollowState.NOT_FOLLOWING) {
getString(R.string.action_follow)
} else {
getString(R.string.action_unfollow)
}
follow.isVisible = followState != FollowState.REQUESTED
val block = menu.findItem(R.id.action_block) val block = menu.findItem(R.id.action_block)
block.title = if (blocking) { block.title = if (blocking) {
@ -666,8 +706,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
} }
} else { } else {
// It shouldn't be possible to block, follow, mute or report yourself. // It shouldn't be possible to block, mute or report yourself.
menu.removeItem(R.id.action_follow)
menu.removeItem(R.id.action_block) menu.removeItem(R.id.action_block)
menu.removeItem(R.id.action_mute) menu.removeItem(R.id.action_mute)
menu.removeItem(R.id.action_mute_domain) menu.removeItem(R.id.action_mute_domain)
@ -722,10 +761,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
if (viewModel.relationshipData.value?.data?.muting != true) { if (viewModel.relationshipData.value?.data?.muting != true) {
loadedAccount?.let { loadedAccount?.let {
showMuteAccountDialog( showMuteAccountDialog(
this, this,
it.username it.username
) { notifications -> ) { notifications, duration ->
viewModel.muteAccount(notifications) viewModel.muteAccount(notifications, duration)
} }
} }
} else { } else {
@ -759,14 +798,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
android.R.id.home -> {
onBackPressed()
return true
}
R.id.action_mention -> {
mention()
return true
}
R.id.action_open_in_web -> { R.id.action_open_in_web -> {
// If the account isn't loaded yet, eat the input. // If the account isn't loaded yet, eat the input.
if (loadedAccount != null) { if (loadedAccount != null) {
@ -774,10 +805,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
} }
return true return true
} }
R.id.action_follow -> {
viewModel.changeFollowState()
return true
}
R.id.action_block -> { R.id.action_block -> {
toggleBlock() toggleBlock()
return true return true

View file

@ -18,7 +18,6 @@ package com.keylesspalace.tusky
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem
import com.keylesspalace.tusky.fragment.AccountListFragment import com.keylesspalace.tusky.fragment.AccountListFragment
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
@ -68,16 +67,6 @@ class AccountListActivity : BaseActivity(), HasAndroidInjector {
.commit() .commit()
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
return true
}
}
return super.onOptionsItemSelected(item)
}
override fun androidInjector() = dispatchingAndroidInjector override fun androidInjector() = dispatchingAndroidInjector
companion object { companion object {

View file

@ -31,6 +31,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
import com.keylesspalace.tusky.viewmodel.State import com.keylesspalace.tusky.viewmodel.State
@ -71,7 +72,9 @@ class AccountsInListFragment : DialogFragment(), Injectable {
private val searchAdapter = SearchAdapter() private val searchAdapter = SearchAdapter()
private val radius by lazy { resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) } private val radius by lazy { resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) }
private val animateAvatar by lazy { PreferenceManager.getDefaultSharedPreferences(requireContext()).getBoolean("animateGifAvatars", false) } private val pm by lazy { PreferenceManager.getDefaultSharedPreferences(requireContext()) }
private val animateAvatar by lazy { pm.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false) }
private val animateEmojis by lazy { pm.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -209,7 +212,7 @@ class AccountsInListFragment : DialogFragment(), Injectable {
} }
fun bind(account: Account) { fun bind(account: Account) {
displayNameTextView.text = account.name.emojify(account.emojis, displayNameTextView) displayNameTextView.text = account.name.emojify(account.emojis, displayNameTextView, animateEmojis)
usernameTextView.text = account.username usernameTextView.text = account.username
loadAvatar(account.avatar, avatar, radius, animateAvatar) loadAvatar(account.avatar, avatar, radius, animateAvatar)
} }
@ -252,7 +255,7 @@ class AccountsInListFragment : DialogFragment(), Injectable {
override val containerView = itemView override val containerView = itemView
fun bind(account: Account, inAList: Boolean) { fun bind(account: Account, inAList: Boolean) {
displayNameTextView.text = account.name.emojify(account.emojis, displayNameTextView) displayNameTextView.text = account.name.emojify(account.emojis, displayNameTextView, animateEmojis)
usernameTextView.text = account.username usernameTextView.text = account.username
loadAvatar(account.avatar, avatar, radius, animateAvatar) loadAvatar(account.avatar, avatar, radius, animateAvatar)

View file

@ -24,6 +24,7 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -127,6 +128,15 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left); overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left);
} }
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
@Override @Override
public void finish() { public void finish() {
super.finish(); super.finish();

View file

@ -296,10 +296,6 @@ class EditProfileActivity : BaseActivity(), Injectable {
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
android.R.id.home -> {
onBackPressed()
return true
}
R.id.action_save -> { R.id.action_save -> {
save() save()
return true return true

View file

@ -1,7 +1,6 @@
package com.keylesspalace.tusky package com.keylesspalace.tusky
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.Toast import android.widget.Toast
@ -205,14 +204,4 @@ class FiltersActivity: BaseActivity() {
} }
} }
// Activate back arrow in toolbar
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
return true
}
}
return super.onOptionsItemSelected(item)
}
} }

View file

@ -18,7 +18,6 @@ package com.keylesspalace.tusky
import android.os.Bundle import android.os.Bundle
import androidx.annotation.RawRes import androidx.annotation.RawRes
import android.util.Log import android.util.Log
import android.view.MenuItem
import android.widget.TextView import android.widget.TextView
import com.keylesspalace.tusky.util.IOUtils import com.keylesspalace.tusky.util.IOUtils
import kotlinx.android.extensions.CacheImplementation import kotlinx.android.extensions.CacheImplementation
@ -48,16 +47,6 @@ class LicenseActivity : BaseActivity() {
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun loadFileIntoTextView(@RawRes fileId: Int, textView: TextView) { private fun loadFileIntoTextView(@RawRes fileId: Int, textView: TextView) {
val sb = StringBuilder() val sb = StringBuilder()

View file

@ -21,7 +21,6 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.* import android.widget.*
@ -130,19 +129,27 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
else R.string.action_rename_list) { _, _ -> else R.string.action_rename_list) { _, _ ->
onPickedDialogName(editText.text, list?.id) onPickedDialogName(editText.text, list?.id)
} }
.setNegativeButton(android.R.string.cancel) { d, _ -> .setNegativeButton(android.R.string.cancel, null)
d.dismiss()
}
.show() .show()
val positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE) val positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE)
editText.onTextChanged { s, _, _, _ -> editText.onTextChanged { s, _, _, _ ->
positiveButton.isEnabled = !s.isBlank() positiveButton.isEnabled = s.isNotBlank()
} }
editText.setText(list?.title) editText.setText(list?.title)
editText.text?.let { editText.setSelection(it.length) } editText.text?.let { editText.setSelection(it.length) }
} }
private fun showListDeleteDialog(list: MastoList) {
AlertDialog.Builder(this)
.setMessage(getString(R.string.dialog_delete_list_warning, list.title))
.setPositiveButton(R.string.action_delete){ _, _ ->
viewModel.deleteList(list.id)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun update(state: ListsViewModel.State) { private fun update(state: ListsViewModel.State) {
adapter.submitList(state.lists) adapter.submitList(state.lists)
@ -199,7 +206,7 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
when (item.itemId) { when (item.itemId) {
R.id.list_edit -> openListSettings(list) R.id.list_edit -> openListSettings(list)
R.id.list_rename -> renameListDialog(list) R.id.list_rename -> renameListDialog(list)
R.id.list_delete -> viewModel.deleteList(list.id) R.id.list_delete -> showListDeleteDialog(list)
else -> return@setOnMenuItemClickListener false else -> return@setOnMenuItemClickListener false
} }
true true
@ -210,14 +217,6 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
override fun androidInjector() = dispatchingAndroidInjector override fun androidInjector() = dispatchingAndroidInjector
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
onBackPressed()
return true
}
return false
}
private object ListsDiffer : DiffUtil.ItemCallback<MastoList>() { private object ListsDiffer : DiffUtil.ItemCallback<MastoList>() {
override fun areItemsTheSame(oldItem: MastoList, newItem: MastoList): Boolean { override fun areItemsTheSame(oldItem: MastoList, newItem: MastoList): Boolean {
return oldItem.id == newItem.id return oldItem.id == newItem.id

View file

@ -20,14 +20,13 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.util.Log import android.util.Log
import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
@ -362,16 +361,19 @@ class LoginActivity : BaseActivity(), Injectable {
private fun openInCustomTab(uri: Uri, context: Context): Boolean { private fun openInCustomTab(uri: Uri, context: Context): Boolean {
val toolbarColor = ThemeUtils.getColor(context, R.attr.colorSurface) val toolbarColor = ThemeUtils.getColor(context, R.attr.colorSurface)
val customTabsIntentBuilder = CustomTabsIntent.Builder() val navigationbarColor = ThemeUtils.getColor(context, android.R.attr.navigationBarColor)
val navigationbarDividerColor = ThemeUtils.getColor(context, R.attr.dividerColor)
val colorSchemeParams = CustomTabColorSchemeParams.Builder()
.setToolbarColor(toolbarColor) .setToolbarColor(toolbarColor)
.setNavigationBarColor(navigationbarColor)
.setNavigationBarDividerColor(navigationbarDividerColor)
.build()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { val customTabsIntent = CustomTabsIntent.Builder()
customTabsIntentBuilder.setNavigationBarColor( .setDefaultColorSchemeParams(colorSchemeParams)
ThemeUtils.getColor(context, android.R.attr.navigationBarColor) .build()
)
}
val customTabsIntent = customTabsIntentBuilder.build()
try { try {
customTabsIntent.launchUrl(context, uri) customTabsIntent.launchUrl(context, uri)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {

View file

@ -31,6 +31,7 @@ import android.widget.ImageView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.emoji.text.EmojiCompat import androidx.emoji.text.EmojiCompat
import androidx.emoji.text.EmojiCompat.InitCallback import androidx.emoji.text.EmojiCompat.InitCallback
@ -52,11 +53,14 @@ import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType
import com.keylesspalace.tusky.components.conversation.ConversationsRepository import com.keylesspalace.tusky.components.conversation.ConversationsRepository
import com.keylesspalace.tusky.components.drafts.DraftHelper
import com.keylesspalace.tusky.components.drafts.DraftsActivity
import com.keylesspalace.tusky.components.notifications.NotificationHelper import com.keylesspalace.tusky.components.notifications.NotificationHelper
import com.keylesspalace.tusky.components.preference.PreferencesActivity import com.keylesspalace.tusky.components.preference.PreferencesActivity
import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity
import com.keylesspalace.tusky.components.search.SearchActivity import com.keylesspalace.tusky.components.search.SearchActivity
import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.fragment.SFragment import com.keylesspalace.tusky.fragment.SFragment
import com.keylesspalace.tusky.interfaces.AccountSelectionListener import com.keylesspalace.tusky.interfaces.AccountSelectionListener
@ -98,6 +102,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
@Inject @Inject
lateinit var conversationRepository: ConversationsRepository lateinit var conversationRepository: ConversationsRepository
@Inject
lateinit var appDb: AppDatabase
@Inject
lateinit var draftHelper: DraftHelper
private lateinit var header: AccountHeaderView private lateinit var header: AccountHeaderView
private var notificationTabPosition = 0 private var notificationTabPosition = 0
@ -229,6 +239,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
// Flush old media that was cached for sharing // Flush old media that was cached for sharing
deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Tusky")) deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Tusky"))
} }
draftWarning()
} }
override fun onResume() { override fun onResume() {
@ -397,7 +408,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
nameRes = R.string.action_access_saved_toot nameRes = R.string.action_access_saved_toot
iconRes = R.drawable.ic_notebook iconRes = R.drawable.ic_notebook
onClick = { onClick = {
val intent = Intent(context, SavedTootActivity::class.java) val intent = DraftsActivity.newIntent(context)
startActivityWithSlideInAnimation(intent) startActivityWithSlideInAnimation(intent)
} }
}, },
@ -554,6 +565,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
val activeTabPosition = if (selectNotificationTab) notificationTabPosition else 0 val activeTabPosition = if (selectNotificationTab) notificationTabPosition else 0
mainToolbar.title = tabs[activeTabPosition].title(this@MainActivity) mainToolbar.title = tabs[activeTabPosition].title(this@MainActivity)
mainToolbar.setOnClickListener {
(adapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect()
}
} }
@ -601,6 +615,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, this) NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, this)
cacheUpdater.clearForUser(activeAccount.id) cacheUpdater.clearForUser(activeAccount.id)
conversationRepository.deleteCacheForAccount(activeAccount.id) conversationRepository.deleteCacheForAccount(activeAccount.id)
draftHelper.deleteAllDraftsAndAttachmentsForAccount(activeAccount.id)
removeShortcut(this, activeAccount) removeShortcut(this, activeAccount)
val newAccount = accountManager.logActiveAccountOut() val newAccount = accountManager.logActiveAccountOut()
if (!NotificationHelper.areNotificationsEnabled(this, accountManager)) { if (!NotificationHelper.areNotificationsEnabled(this, accountManager)) {
@ -679,16 +694,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
.into(object : CustomTarget<Drawable>(navIconSize, navIconSize) { .into(object : CustomTarget<Drawable>(navIconSize, navIconSize) {
override fun onLoadStarted(placeholder: Drawable?) { override fun onLoadStarted(placeholder: Drawable?) {
if(placeholder != null) { if (placeholder != null) {
mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
} }
} }
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) { override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
mainToolbar.navigationIcon = resource mainToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize)
} }
override fun onLoadCleared(placeholder: Drawable?) { override fun onLoadCleared(placeholder: Drawable?) {
mainToolbar.navigationIcon = placeholder if (placeholder != null) {
mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
}
} }
}) })
} }
@ -713,8 +730,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
} }
private fun updateProfiles() { private fun updateProfiles() {
val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
val profiles: MutableList<IProfile> = accountManager.getAllAccountsOrderedByActive().map { acc -> val profiles: MutableList<IProfile> = accountManager.getAllAccountsOrderedByActive().map { acc ->
val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header)) val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header, animateEmojis))
ProfileDrawerItem().apply { ProfileDrawerItem().apply {
isSelected = acc.isActive isSelected = acc.isActive
@ -738,6 +756,29 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
header.setActiveProfile(accountManager.activeAccount!!.id) header.setActiveProfile(accountManager.activeAccount!!.id)
} }
private fun draftWarning() {
val sharedPrefsKey = "show_draft_warning"
appDb.tootDao().savedTootCount()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
.subscribe { draftCount ->
val showDraftWarning = preferences.getBoolean(sharedPrefsKey, true)
if (draftCount > 0 && showDraftWarning) {
AlertDialog.Builder(this)
.setMessage(R.string.new_drafts_warning)
.setNegativeButton("Don't show again") { _, _ ->
preferences.edit(commit = true) {
putBoolean(sharedPrefsKey, false)
}
}
.setPositiveButton(android.R.string.ok, null)
.show()
}
}
}
override fun getActionButton(): FloatingActionButton? = composeButton override fun getActionButton(): FloatingActionButton? = composeButton
override fun androidInjector() = androidInjector override fun androidInjector() = androidInjector

View file

@ -3,7 +3,6 @@ package com.keylesspalace.tusky
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.keylesspalace.tusky.fragment.TimelineFragment import com.keylesspalace.tusky.fragment.TimelineFragment
import com.keylesspalace.tusky.interfaces.ActionButtonActivity import com.keylesspalace.tusky.interfaces.ActionButtonActivity
@ -56,14 +55,6 @@ class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAn
override fun getActionButton(): FloatingActionButton? = null override fun getActionButton(): FloatingActionButton? = null
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
onBackPressed()
return true
}
return false
}
override fun androidInjector() = dispatchingAndroidInjector override fun androidInjector() = dispatchingAndroidInjector
} }

View file

@ -18,7 +18,6 @@ package com.keylesspalace.tusky;
import android.content.Intent; import android.content.Intent;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -89,7 +88,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd
setSupportActionBar(toolbar); setSupportActionBar(toolbar);
ActionBar bar = getSupportActionBar(); ActionBar bar = getSupportActionBar();
if (bar != null) { if (bar != null) {
bar.setTitle(getString(R.string.title_saved_toot)); bar.setTitle(getString(R.string.title_drafts));
bar.setDisplayHomeAsUpEnabled(true); bar.setDisplayHomeAsUpEnabled(true);
bar.setDisplayShowHomeEnabled(true); bar.setDisplayShowHomeEnabled(true);
} }
@ -118,17 +117,6 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd
if (asyncTask != null) asyncTask.cancel(true); if (asyncTask != null) asyncTask.cancel(true);
} }
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home: {
onBackPressed();
return true;
}
}
return super.onOptionsItemSelected(item);
}
private void fetchToots() { private void fetchToots() {
asyncTask = new FetchPojosTask(this, database.tootDao()) asyncTask = new FetchPojosTask(this, database.tootDao())
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
@ -166,6 +154,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd
ComposeOptions composeOptions = new ComposeOptions( ComposeOptions composeOptions = new ComposeOptions(
/*scheduledTootUid*/null, /*scheduledTootUid*/null,
item.getUid(), item.getUid(),
/*drafId*/null,
item.getText(), item.getText(),
jsonUrls, jsonUrls,
descriptions, descriptions,
@ -177,6 +166,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd
item.getInReplyToUsername(), item.getInReplyToUsername(),
item.getInReplyToText(), item.getInReplyToText(),
/*mediaAttachments*/null, /*mediaAttachments*/null,
/*draftAttachments*/null,
/*scheduledAt*/null, /*scheduledAt*/null,
/*sensitive*/null, /*sensitive*/null,
/*poll*/null, /*poll*/null,

View file

@ -18,7 +18,6 @@ package com.keylesspalace.tusky
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem
import androidx.fragment.app.commit import androidx.fragment.app.commit
import com.keylesspalace.tusky.fragment.TimelineFragment import com.keylesspalace.tusky.fragment.TimelineFragment
@ -66,14 +65,6 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home){
onBackPressed()
return true
}
return super.onOptionsItemSelected(item)
}
override fun androidInjector() = dispatchingAndroidInjector override fun androidInjector() = dispatchingAndroidInjector
companion object { companion object {

View file

@ -18,7 +18,6 @@ package com.keylesspalace.tusky
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
@ -345,14 +344,6 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
} }
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
onBackPressed()
return true
}
return false
}
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
if (tabsChanged) { if (tabsChanged) {

View file

@ -18,7 +18,6 @@ package com.keylesspalace.tusky;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.view.MenuItem;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBar;
@ -72,17 +71,6 @@ public class ViewTagActivity extends BottomSheetActivity implements HasAndroidIn
fragmentTransaction.commit(); fragmentTransaction.commit();
} }
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home: {
onBackPressed();
return true;
}
}
return super.onOptionsItemSelected(item);
}
@Override @Override
public AndroidInjector<Object> androidInjector() { public AndroidInjector<Object> androidInjector() {
return dispatchingAndroidInjector; return dispatchingAndroidInjector;

View file

@ -110,10 +110,6 @@ public class ViewThreadActivity extends BottomSheetActivity implements HasAndroi
@Override @Override
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { switch (item.getItemId()) {
case android.R.id.home: {
onBackPressed();
return true;
}
case R.id.action_open_in_web: { case R.id.action_open_in_web: {
LinkHelper.openLink(getIntent().getStringExtra(URL_EXTRA), this); LinkHelper.openLink(getIntent().getStringExtra(URL_EXTRA), this);
return true; return true;

View file

@ -33,10 +33,14 @@ public abstract class AccountAdapter extends RecyclerView.Adapter {
List<Account> accountList; List<Account> accountList;
AccountActionListener accountActionListener; AccountActionListener accountActionListener;
private boolean bottomLoading; private boolean bottomLoading;
protected final boolean animateEmojis;
protected final boolean animateAvatar;
AccountAdapter(AccountActionListener accountActionListener) { AccountAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) {
this.accountList = new ArrayList<>(); this.accountList = new ArrayList<>();
this.accountActionListener = accountActionListener; this.accountActionListener = accountActionListener;
this.animateAvatar = animateAvatar;
this.animateEmojis = animateEmojis;
bottomLoading = false; bottomLoading = false;
} }

View file

@ -29,7 +29,7 @@ import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.*
import kotlinx.android.synthetic.main.item_account_field.view.* import kotlinx.android.synthetic.main.item_account_field.view.*
class AccountFieldAdapter(private val linkListener: LinkListener) : RecyclerView.Adapter<AccountFieldAdapter.ViewHolder>() { class AccountFieldAdapter(private val linkListener: LinkListener, private val animateEmojis: Boolean) : RecyclerView.Adapter<AccountFieldAdapter.ViewHolder>() {
var emojis: List<Emoji> = emptyList() var emojis: List<Emoji> = emptyList()
var fields: List<Either<IdentityProof, Field>> = emptyList() var fields: List<Either<IdentityProof, Field>> = emptyList()
@ -55,10 +55,10 @@ class AccountFieldAdapter(private val linkListener: LinkListener) : RecyclerView
viewHolder.valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0) viewHolder.valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)
} else { } else {
val field = proofOrField.asRight() val field = proofOrField.asRight()
val emojifiedName = field.name.emojify(emojis, viewHolder.nameTextView) val emojifiedName = field.name.emojify(emojis, viewHolder.nameTextView, animateEmojis)
viewHolder.nameTextView.text = emojifiedName viewHolder.nameTextView.text = emojifiedName
val emojifiedValue = field.value.emojify(emojis, viewHolder.valueTextView) val emojifiedValue = field.value.emojify(emojis, viewHolder.valueTextView, animateEmojis)
LinkHelper.setClickableText(viewHolder.valueTextView, emojifiedValue, null, linkListener) LinkHelper.setClickableText(viewHolder.valueTextView, emojifiedValue, null, linkListener)
if(field.verifiedAt != null) { if(field.verifiedAt != null) {

View file

@ -23,6 +23,7 @@ import android.widget.ArrayAdapter
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.*
import kotlinx.android.synthetic.main.item_autocomplete_account.view.* import kotlinx.android.synthetic.main.item_autocomplete_account.view.*
@ -41,12 +42,14 @@ class AccountSelectionAdapter(context: Context) : ArrayAdapter<AccountEntity>(co
val username = view.username val username = view.username
val displayName = view.display_name val displayName = view.display_name
val avatar = view.avatar val avatar = view.avatar
val pm = PreferenceManager.getDefaultSharedPreferences(avatar.context)
val animateEmojis = pm.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
username.text = account.fullName username.text = account.fullName
displayName.text = account.displayName.emojify(account.emojis, displayName) displayName.text = account.displayName.emojify(account.emojis, displayName, animateEmojis)
val avatarRadius = avatar.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_42dp) val avatarRadius = avatar.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_42dp)
val animateAvatar = PreferenceManager.getDefaultSharedPreferences(avatar.context) val animateAvatar = pm.getBoolean("animateGifAvatars", false)
.getBoolean("animateGifAvatars", false)
loadAvatar(account.profilePictureUrl, avatar, avatarRadius, animateAvatar) loadAvatar(account.profilePictureUrl, avatar, avatarRadius, animateAvatar)

View file

@ -22,7 +22,6 @@ public class AccountViewHolder extends RecyclerView.ViewHolder {
private ImageView avatarInset; private ImageView avatarInset;
private String accountId; private String accountId;
private boolean showBotOverlay; private boolean showBotOverlay;
private boolean animateAvatar;
public AccountViewHolder(View itemView) { public AccountViewHolder(View itemView) {
super(itemView); super(itemView);
@ -32,15 +31,14 @@ public class AccountViewHolder extends RecyclerView.ViewHolder {
avatarInset = itemView.findViewById(R.id.account_avatar_inset); avatarInset = itemView.findViewById(R.id.account_avatar_inset);
SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(itemView.getContext()); SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(itemView.getContext());
showBotOverlay = sharedPrefs.getBoolean("showBotOverlay", true); showBotOverlay = sharedPrefs.getBoolean("showBotOverlay", true);
animateAvatar = sharedPrefs.getBoolean("animateGifAvatars", false);
} }
public void setupWithAccount(Account account) { public void setupWithAccount(Account account, boolean animateAvatar, boolean animateEmojis) {
accountId = account.getId(); accountId = account.getId();
String format = username.getContext().getString(R.string.status_username_format); String format = username.getContext().getString(R.string.status_username_format);
String formattedUsername = String.format(format, account.getUsername()); String formattedUsername = String.format(format, account.getUsername());
username.setText(formattedUsername); username.setText(formattedUsername);
CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName); CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, animateEmojis);
displayName.setText(emojifiedName); displayName.setText(emojifiedName);
int avatarRadius = avatar.getContext().getResources() int avatarRadius = avatar.getContext().getResources()
.getDimensionPixelSize(R.dimen.avatar_radius_48dp); .getDimensionPixelSize(R.dimen.avatar_radius_48dp);

View file

@ -34,8 +34,8 @@ import com.keylesspalace.tusky.util.ImageLoadingHelper;
public class BlocksAdapter extends AccountAdapter { public class BlocksAdapter extends AccountAdapter {
public BlocksAdapter(AccountActionListener accountActionListener) { public BlocksAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) {
super(accountActionListener); super(accountActionListener, animateAvatar, animateEmojis);
} }
@NonNull @NonNull
@ -60,7 +60,7 @@ public class BlocksAdapter extends AccountAdapter {
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
BlockedUserViewHolder holder = (BlockedUserViewHolder) viewHolder; BlockedUserViewHolder holder = (BlockedUserViewHolder) viewHolder;
holder.setupWithAccount(accountList.get(position)); holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis);
holder.setupActionListener(accountActionListener); holder.setupActionListener(accountActionListener);
} }
} }
@ -71,7 +71,6 @@ public class BlocksAdapter extends AccountAdapter {
private TextView displayName; private TextView displayName;
private ImageButton unblock; private ImageButton unblock;
private String id; private String id;
private boolean animateAvatar;
BlockedUserViewHolder(View itemView) { BlockedUserViewHolder(View itemView) {
super(itemView); super(itemView);
@ -79,14 +78,12 @@ public class BlocksAdapter extends AccountAdapter {
username = itemView.findViewById(R.id.blocked_user_username); username = itemView.findViewById(R.id.blocked_user_username);
displayName = itemView.findViewById(R.id.blocked_user_display_name); displayName = itemView.findViewById(R.id.blocked_user_display_name);
unblock = itemView.findViewById(R.id.blocked_user_unblock); unblock = itemView.findViewById(R.id.blocked_user_unblock);
animateAvatar = PreferenceManager.getDefaultSharedPreferences(itemView.getContext())
.getBoolean("animateGifAvatars", false);
} }
void setupWithAccount(Account account) { void setupWithAccount(Account account, boolean animateAvatar, boolean animateEmojis) {
id = account.getId(); id = account.getId();
CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName); CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, animateEmojis);
displayName.setText(emojifiedName); displayName.setText(emojifiedName);
String format = username.getContext().getString(R.string.status_username_format); String format = username.getContext().getString(R.string.status_username_format);
String formattedUsername = String.format(format, account.getUsername()); String formattedUsername = String.format(format, account.getUsername());

View file

@ -27,8 +27,8 @@ import com.keylesspalace.tusky.interfaces.AccountActionListener;
/** Both for follows and following lists. */ /** Both for follows and following lists. */
public class FollowAdapter extends AccountAdapter { public class FollowAdapter extends AccountAdapter {
public FollowAdapter(AccountActionListener accountActionListener) { public FollowAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) {
super(accountActionListener); super(accountActionListener, animateAvatar, animateEmojis);
} }
@NonNull @NonNull
@ -53,7 +53,7 @@ public class FollowAdapter extends AccountAdapter {
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
AccountViewHolder holder = (AccountViewHolder) viewHolder; AccountViewHolder holder = (AccountViewHolder) viewHolder;
holder.setupWithAccount(accountList.get(position)); holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis);
holder.setupActionListener(accountActionListener); holder.setupActionListener(accountActionListener);
} }
} }

View file

@ -10,27 +10,24 @@ import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.interfaces.AccountActionListener import com.keylesspalace.tusky.interfaces.AccountActionListener
import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.loadAvatar
import com.keylesspalace.tusky.util.unicodeWrap
import com.keylesspalace.tusky.util.visible
import kotlinx.android.synthetic.main.item_follow_request_notification.view.* import kotlinx.android.synthetic.main.item_follow_request_notification.view.*
internal class FollowRequestViewHolder(itemView: View, private val showHeader: Boolean) : RecyclerView.ViewHolder(itemView) { internal class FollowRequestViewHolder(
itemView: View,
private val showHeader: Boolean) : RecyclerView.ViewHolder(itemView) {
private var id: String? = null private var id: String? = null
private val animateAvatar: Boolean = PreferenceManager.getDefaultSharedPreferences(itemView.context)
.getBoolean("animateGifAvatars", false)
fun setupWithAccount(account: Account) { fun setupWithAccount(account: Account, animateAvatar: Boolean, animateEmojis: Boolean) {
id = account.id id = account.id
val wrappedName = account.name.unicodeWrap() val wrappedName = account.name.unicodeWrap()
val emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView) val emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView, animateEmojis)
itemView.displayNameTextView.text = emojifiedName itemView.displayNameTextView.text = emojifiedName
if (showHeader) { if (showHeader) {
val wholeMessage: String = itemView.context.getString(R.string.notification_follow_request_format, wrappedName) val wholeMessage: String = itemView.context.getString(R.string.notification_follow_request_format, wrappedName)
itemView.notificationTextView?.text = SpannableStringBuilder(wholeMessage).apply { itemView.notificationTextView?.text = SpannableStringBuilder(wholeMessage).apply {
setSpan(StyleSpan(Typeface.BOLD), 0, wrappedName.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) setSpan(StyleSpan(Typeface.BOLD), 0, wrappedName.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}.emojify(account.emojis, itemView) }.emojify(account.emojis, itemView, animateEmojis)
} }
itemView.notificationTextView?.visible(showHeader) itemView.notificationTextView?.visible(showHeader)
val format = itemView.context.getString(R.string.status_username_format) val format = itemView.context.getString(R.string.status_username_format)

View file

@ -27,8 +27,8 @@ import com.keylesspalace.tusky.interfaces.AccountActionListener;
public class FollowRequestsAdapter extends AccountAdapter { public class FollowRequestsAdapter extends AccountAdapter {
public FollowRequestsAdapter(AccountActionListener accountActionListener) { public FollowRequestsAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) {
super(accountActionListener); super(accountActionListener, animateAvatar, animateEmojis);
} }
@NonNull @NonNull
@ -53,7 +53,7 @@ public class FollowRequestsAdapter extends AccountAdapter {
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder; FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
holder.setupWithAccount(accountList.get(position)); holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis);
holder.setupActionListener(accountActionListener); holder.setupActionListener(accountActionListener);
} }
} }

View file

@ -23,8 +23,8 @@ import java.util.HashMap;
public class MutesAdapter extends AccountAdapter { public class MutesAdapter extends AccountAdapter {
private HashMap<String, Boolean> mutingNotificationsMap; private HashMap<String, Boolean> mutingNotificationsMap;
public MutesAdapter(AccountActionListener accountActionListener) { public MutesAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) {
super(accountActionListener); super(accountActionListener, animateAvatar, animateEmojis);
mutingNotificationsMap = new HashMap<String, Boolean>(); mutingNotificationsMap = new HashMap<String, Boolean>();
} }
@ -51,7 +51,7 @@ public class MutesAdapter extends AccountAdapter {
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
MutedUserViewHolder holder = (MutedUserViewHolder) viewHolder; MutedUserViewHolder holder = (MutedUserViewHolder) viewHolder;
Account account = accountList.get(position); Account account = accountList.get(position);
holder.setupWithAccount(account, mutingNotificationsMap.get(account.getId())); holder.setupWithAccount(account, mutingNotificationsMap.get(account.getId()), animateAvatar, animateEmojis);
holder.setupActionListener(accountActionListener); holder.setupActionListener(accountActionListener);
} }
} }
@ -73,7 +73,6 @@ public class MutesAdapter extends AccountAdapter {
private ImageButton unmute; private ImageButton unmute;
private ImageButton muteNotifications; private ImageButton muteNotifications;
private String id; private String id;
private boolean animateAvatar;
private boolean notifications; private boolean notifications;
MutedUserViewHolder(View itemView) { MutedUserViewHolder(View itemView) {
@ -83,13 +82,11 @@ public class MutesAdapter extends AccountAdapter {
displayName = itemView.findViewById(R.id.muted_user_display_name); displayName = itemView.findViewById(R.id.muted_user_display_name);
unmute = itemView.findViewById(R.id.muted_user_unmute); unmute = itemView.findViewById(R.id.muted_user_unmute);
muteNotifications = itemView.findViewById(R.id.muted_user_mute_notifications); muteNotifications = itemView.findViewById(R.id.muted_user_mute_notifications);
animateAvatar = PreferenceManager.getDefaultSharedPreferences(itemView.getContext())
.getBoolean("animateGifAvatars", false);
} }
void setupWithAccount(Account account, Boolean mutingNotifications) { void setupWithAccount(Account account, Boolean mutingNotifications, boolean animateAvatar, boolean animateEmojis) {
id = account.getId(); id = account.getId();
CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName); CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, animateEmojis);
displayName.setText(emojifiedName); displayName.setText(emojifiedName);
String format = username.getContext().getString(R.string.status_username_format); String format = username.getContext().getString(R.string.status_username_format);
String formattedUsername = String.format(format, account.getUsername()); String formattedUsername = String.format(format, account.getUsername());

View file

@ -37,6 +37,7 @@ import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Emoji; import com.keylesspalace.tusky.entity.Emoji;
@ -198,8 +199,12 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
holder.setUsername(statusViewData.getNickname()); holder.setUsername(statusViewData.getNickname());
holder.setCreatedAt(statusViewData.getCreatedAt()); holder.setCreatedAt(statusViewData.getCreatedAt());
holder.setAvatars(concreteNotificaton.getStatusViewData().getAvatar(), if(concreteNotificaton.getType() == Notification.Type.STATUS) {
concreteNotificaton.getAccount().getAvatar()); holder.setAvatar(statusViewData.getAvatar(), statusViewData.isBot());
} else {
holder.setAvatars(statusViewData.getAvatar(),
concreteNotificaton.getAccount().getAvatar());
}
} }
holder.setMessage(concreteNotificaton, statusListener); holder.setMessage(concreteNotificaton, statusListener);
@ -227,7 +232,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
case VIEW_TYPE_FOLLOW_REQUEST: { case VIEW_TYPE_FOLLOW_REQUEST: {
if (payloadForHolder == null) { if (payloadForHolder == null) {
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder; FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
holder.setupWithAccount(concreteNotificaton.getAccount()); holder.setupWithAccount(concreteNotificaton.getAccount(), statusDisplayOptions.animateAvatars(), statusDisplayOptions.animateEmojis());
holder.setupActionListener(accountActionListener); holder.setupActionListener(accountActionListener);
} }
} }
@ -249,7 +254,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
statusDisplayOptions.showBotOverlay(), statusDisplayOptions.showBotOverlay(),
statusDisplayOptions.useBlurhash(), statusDisplayOptions.useBlurhash(),
CardViewMode.NONE, CardViewMode.NONE,
statusDisplayOptions.confirmReblogs() statusDisplayOptions.confirmReblogs(),
statusDisplayOptions.hideStats(),
statusDisplayOptions.animateEmojis()
); );
} }
@ -267,6 +274,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
case POLL: { case POLL: {
return VIEW_TYPE_STATUS; return VIEW_TYPE_STATUS;
} }
case STATUS:
case FAVOURITE: case FAVOURITE:
case REBLOG: { case REBLOG: {
return VIEW_TYPE_STATUS_NOTIFICATION; return VIEW_TYPE_STATUS_NOTIFICATION;
@ -329,13 +337,17 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
String format = context.getString(R.string.notification_follow_format); String format = context.getString(R.string.notification_follow_format);
String wrappedDisplayName = StringUtils.unicodeWrap(account.getName()); String wrappedDisplayName = StringUtils.unicodeWrap(account.getName());
String wholeMessage = String.format(format, wrappedDisplayName); String wholeMessage = String.format(format, wrappedDisplayName);
CharSequence emojifiedMessage = CustomEmojiHelper.emojify(wholeMessage, account.getEmojis(), message); CharSequence emojifiedMessage = CustomEmojiHelper.emojify(
wholeMessage, account.getEmojis(), message, statusDisplayOptions.animateEmojis()
);
message.setText(emojifiedMessage); message.setText(emojifiedMessage);
String username = context.getString(R.string.status_username_format, account.getUsername()); String username = context.getString(R.string.status_username_format, account.getUsername());
usernameView.setText(username); usernameView.setText(username);
CharSequence emojifiedDisplayName = CustomEmojiHelper.emojify(wrappedDisplayName, account.getEmojis(), usernameView); CharSequence emojifiedDisplayName = CustomEmojiHelper.emojify(
wrappedDisplayName, account.getEmojis(), usernameView, statusDisplayOptions.animateEmojis()
);
displayNameView.setText(emojifiedDisplayName); displayNameView.setText(emojifiedDisplayName);
@ -373,6 +385,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
private StatusViewData.Concrete statusViewData; private StatusViewData.Concrete statusViewData;
private SimpleDateFormat shortSdf; private SimpleDateFormat shortSdf;
private SimpleDateFormat longSdf; private SimpleDateFormat longSdf;
private int avatarRadius48dp;
private int avatarRadius36dp;
private int avatarRadius24dp;
StatusNotificationViewHolder(View itemView, StatusDisplayOptions statusDisplayOptions) { StatusNotificationViewHolder(View itemView, StatusDisplayOptions statusDisplayOptions) {
super(itemView); super(itemView);
@ -398,6 +414,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
statusContent.setOnClickListener(this); statusContent.setOnClickListener(this);
shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()); shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault()); longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault());
this.avatarRadius48dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_48dp);
this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp);
this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp);
} }
private void showNotificationContent(boolean show) { private void showNotificationContent(boolean show) {
@ -410,7 +430,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
} }
private void setDisplayName(String name, List<Emoji> emojis) { private void setDisplayName(String name, List<Emoji> emojis) {
CharSequence emojifiedName = CustomEmojiHelper.emojify(name, emojis, displayName); CharSequence emojifiedName = CustomEmojiHelper.emojify(name, emojis, displayName, statusDisplayOptions.animateEmojis());
displayName.setText(emojifiedName); displayName.setText(emojifiedName);
} }
@ -488,13 +508,25 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
format = context.getString(R.string.notification_reblog_format); format = context.getString(R.string.notification_reblog_format);
break; break;
} }
case STATUS: {
icon = ContextCompat.getDrawable(context, R.drawable.ic_home_24dp);
if (icon != null) {
icon.setColorFilter(ContextCompat.getColor(context,
R.color.tusky_blue), PorterDuff.Mode.SRC_ATOP);
}
format = context.getString(R.string.notification_subscription_format);
break;
}
} }
message.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null); message.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
String wholeMessage = String.format(format, displayName); String wholeMessage = String.format(format, displayName);
final SpannableStringBuilder str = new SpannableStringBuilder(wholeMessage); final SpannableStringBuilder str = new SpannableStringBuilder(wholeMessage);
str.setSpan(new StyleSpan(Typeface.BOLD), 0, displayName.length(), str.setSpan(new StyleSpan(Typeface.BOLD), 0, displayName.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
CharSequence emojifiedText = CustomEmojiHelper.emojify(str, notificationViewData.getAccount().getEmojis(), message); CharSequence emojifiedText = CustomEmojiHelper.emojify(
str, notificationViewData.getAccount().getEmojis(), message, statusDisplayOptions.animateEmojis()
);
message.setText(emojifiedText); message.setText(emojifiedText);
if (statusViewData != null) { if (statusViewData != null) {
@ -526,19 +558,34 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
this.notificationId = notificationId; this.notificationId = notificationId;
} }
void setAvatars(@Nullable String statusAvatarUrl, @Nullable String notificationAvatarUrl) { void setAvatar(@Nullable String statusAvatarUrl, boolean isBot) {
statusAvatar.setPaddingRelative(0, 0, 0, 0);
int statusAvatarRadius = statusAvatar.getContext().getResources()
.getDimensionPixelSize(R.dimen.avatar_radius_36dp);
ImageLoadingHelper.loadAvatar(statusAvatarUrl, ImageLoadingHelper.loadAvatar(statusAvatarUrl,
statusAvatar, statusAvatarRadius, statusDisplayOptions.animateAvatars()); statusAvatar, avatarRadius48dp, statusDisplayOptions.animateAvatars());
int notificationAvatarRadius = statusAvatar.getContext().getResources() if (statusDisplayOptions.showBotOverlay() && isBot) {
.getDimensionPixelSize(R.dimen.avatar_radius_24dp); notificationAvatar.setVisibility(View.VISIBLE);
notificationAvatar.setBackgroundColor(0x50ffffff);
Glide.with(notificationAvatar)
.load(R.drawable.ic_bot_24dp)
.into(notificationAvatar);
} else {
notificationAvatar.setVisibility(View.GONE);
}
}
void setAvatars(@Nullable String statusAvatarUrl, @Nullable String notificationAvatarUrl) {
int padding = Utils.dpToPx(statusAvatar.getContext(), 12);
statusAvatar.setPaddingRelative(0, 0, padding, padding);
ImageLoadingHelper.loadAvatar(statusAvatarUrl,
statusAvatar, avatarRadius36dp, statusDisplayOptions.animateAvatars());
notificationAvatar.setVisibility(View.VISIBLE);
ImageLoadingHelper.loadAvatar(notificationAvatarUrl, notificationAvatar, ImageLoadingHelper.loadAvatar(notificationAvatarUrl, notificationAvatar,
notificationAvatarRadius, statusDisplayOptions.animateAvatars()); avatarRadius24dp, statusDisplayOptions.animateAvatars());
} }
@Override @Override
@ -590,11 +637,17 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
statusContent.setFilters(NO_INPUT_FILTER); statusContent.setFilters(NO_INPUT_FILTER);
} }
CharSequence emojifiedText = CustomEmojiHelper.emojify(content, emojis, statusContent); CharSequence emojifiedText = CustomEmojiHelper.emojify(
content, emojis, statusContent, statusDisplayOptions.animateEmojis()
);
LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getMentions(), listener); LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getMentions(), listener);
CharSequence emojifiedContentWarning = CharSequence emojifiedContentWarning = CustomEmojiHelper.emojify(
CustomEmojiHelper.emojify(statusViewData.getSpoilerText(), statusViewData.getStatusEmojis(), contentWarningDescriptionTextView); statusViewData.getSpoilerText(),
statusViewData.getStatusEmojis(),
contentWarningDescriptionTextView,
statusDisplayOptions.animateEmojis()
);
contentWarningDescriptionTextView.setText(emojifiedContentWarning); contentWarningDescriptionTextView.setText(emojifiedContentWarning);
} }

View file

@ -38,6 +38,7 @@ class PollAdapter: RecyclerView.Adapter<PollViewHolder>() {
private var mode = RESULT private var mode = RESULT
private var emojis: List<Emoji> = emptyList() private var emojis: List<Emoji> = emptyList()
private var resultClickListener: View.OnClickListener? = null private var resultClickListener: View.OnClickListener? = null
private var animateEmojis = false
fun setup( fun setup(
options: List<PollOptionViewData>, options: List<PollOptionViewData>,
@ -45,13 +46,15 @@ class PollAdapter: RecyclerView.Adapter<PollViewHolder>() {
votersCount: Int?, votersCount: Int?,
emojis: List<Emoji>, emojis: List<Emoji>,
mode: Int, mode: Int,
resultClickListener: View.OnClickListener?) { resultClickListener: View.OnClickListener?,
animateEmojis: Boolean) {
this.pollOptions = options this.pollOptions = options
this.voteCount = voteCount this.voteCount = voteCount
this.votersCount = votersCount this.votersCount = votersCount
this.emojis = emojis this.emojis = emojis
this.mode = mode this.mode = mode
this.resultClickListener = resultClickListener this.resultClickListener = resultClickListener
this.animateEmojis = animateEmojis
notifyDataSetChanged() notifyDataSetChanged()
} }
@ -81,7 +84,7 @@ class PollAdapter: RecyclerView.Adapter<PollViewHolder>() {
RESULT -> { RESULT -> {
val percent = calculatePercent(option.votesCount, votersCount, voteCount) val percent = calculatePercent(option.votesCount, votersCount, voteCount)
val emojifiedPollOptionText = buildDescription(option.title, percent, holder.resultTextView.context) val emojifiedPollOptionText = buildDescription(option.title, percent, holder.resultTextView.context)
.emojify(emojis, holder.resultTextView) .emojify(emojis, holder.resultTextView, animateEmojis)
holder.resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText) holder.resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText)
val level = percent * 100 val level = percent * 100
@ -90,7 +93,7 @@ class PollAdapter: RecyclerView.Adapter<PollViewHolder>() {
holder.resultTextView.setOnClickListener(resultClickListener) holder.resultTextView.setOnClickListener(resultClickListener)
} }
SINGLE -> { SINGLE -> {
val emojifiedPollOptionText = option.title.emojify(emojis, holder.radioButton) val emojifiedPollOptionText = option.title.emojify(emojis, holder.radioButton, animateEmojis)
holder.radioButton.text = EmojiCompat.get().process(emojifiedPollOptionText) holder.radioButton.text = EmojiCompat.get().process(emojifiedPollOptionText)
holder.radioButton.isChecked = option.selected holder.radioButton.isChecked = option.selected
holder.radioButton.setOnClickListener { holder.radioButton.setOnClickListener {
@ -101,7 +104,7 @@ class PollAdapter: RecyclerView.Adapter<PollViewHolder>() {
} }
} }
MULTIPLE -> { MULTIPLE -> {
val emojifiedPollOptionText = option.title.emojify(emojis, holder.checkBox) val emojifiedPollOptionText = option.title.emojify(emojis, holder.checkBox, animateEmojis)
holder.checkBox.text = EmojiCompat.get().process(emojifiedPollOptionText) holder.checkBox.text = EmojiCompat.get().process(emojifiedPollOptionText)
holder.checkBox.isChecked = option.selected holder.checkBox.isChecked = option.selected
holder.checkBox.setOnCheckedChangeListener { _, isChecked -> holder.checkBox.setOnCheckedChangeListener { _, isChecked ->

View file

@ -181,8 +181,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
protected abstract int getMediaPreviewHeight(Context context); protected abstract int getMediaPreviewHeight(Context context);
protected void setDisplayName(String name, List<Emoji> customEmojis) { protected void setDisplayName(String name, List<Emoji> customEmojis, StatusDisplayOptions statusDisplayOptions) {
CharSequence emojifiedName = CustomEmojiHelper.emojify(name, customEmojis, displayName); CharSequence emojifiedName = CustomEmojiHelper.emojify(
name, customEmojis, displayName, statusDisplayOptions.animateEmojis()
);
displayName.setText(emojifiedName); displayName.setText(emojifiedName);
} }
@ -206,7 +208,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
final StatusActionListener listener) { final StatusActionListener listener) {
boolean sensitive = !TextUtils.isEmpty(spoilerText); boolean sensitive = !TextUtils.isEmpty(spoilerText);
if (sensitive) { if (sensitive) {
CharSequence emojiSpoiler = CustomEmojiHelper.emojify(spoilerText, emojis, contentWarningDescription); CharSequence emojiSpoiler = CustomEmojiHelper.emojify(
spoilerText, emojis, contentWarningDescription, statusDisplayOptions.animateEmojis()
);
contentWarningDescription.setText(emojiSpoiler); contentWarningDescription.setText(emojiSpoiler);
contentWarningDescription.setVisibility(View.VISIBLE); contentWarningDescription.setVisibility(View.VISIBLE);
contentWarningButton.setVisibility(View.VISIBLE); contentWarningButton.setVisibility(View.VISIBLE);
@ -245,7 +249,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
StatusDisplayOptions statusDisplayOptions, StatusDisplayOptions statusDisplayOptions,
final StatusActionListener listener) { final StatusActionListener listener) {
if (expanded) { if (expanded) {
CharSequence emojifiedText = CustomEmojiHelper.emojify(content, emojis, this.content); CharSequence emojifiedText = CustomEmojiHelper.emojify(content, emojis, this.content, statusDisplayOptions.animateEmojis());
LinkHelper.setClickableText(this.content, emojifiedText, mentions, listener); LinkHelper.setClickableText(this.content, emojifiedText, mentions, listener);
for (int i = 0; i < mediaLabels.length; ++i) { for (int i = 0; i < mediaLabels.length; ++i) {
updateMediaLabel(i, sensitive, expanded); updateMediaLabel(i, sensitive, expanded);
@ -533,7 +537,6 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
@DrawableRes @DrawableRes
private static int getLabelIcon(Attachment.Type type) { private static int getLabelIcon(Attachment.Type type) {
switch (type) { switch (type) {
default:
case IMAGE: case IMAGE:
return R.drawable.ic_photo_24dp; return R.drawable.ic_photo_24dp;
case GIFV: case GIFV:
@ -541,6 +544,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
return R.drawable.ic_videocam_24dp; return R.drawable.ic_videocam_24dp;
case AUDIO: case AUDIO:
return R.drawable.ic_music_box_24dp; return R.drawable.ic_music_box_24dp;
default:
return R.drawable.ic_attach_file_24dp;
} }
} }
@ -708,7 +713,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
StatusDisplayOptions statusDisplayOptions, StatusDisplayOptions statusDisplayOptions,
@Nullable Object payloads) { @Nullable Object payloads) {
if (payloads == null) { if (payloads == null) {
setDisplayName(status.getUserFullName(), status.getAccountEmojis()); setDisplayName(status.getUserFullName(), status.getAccountEmojis(), statusDisplayOptions);
setUsername(status.getNickname()); setUsername(status.getNickname());
setCreatedAt(status.getCreatedAt(), statusDisplayOptions); setCreatedAt(status.getCreatedAt(), statusDisplayOptions);
setIsReply(status.getInReplyToId() != null); setIsReply(status.getInReplyToId() != null);
@ -718,7 +723,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
setBookmarked(status.isBookmarked()); setBookmarked(status.isBookmarked());
List<Attachment> attachments = status.getAttachments(); List<Attachment> attachments = status.getAttachments();
boolean sensitive = status.isSensitive(); boolean sensitive = status.isSensitive();
if (statusDisplayOptions.mediaPreviewEnabled() && !hasAudioAttachment(attachments)) { if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) {
setMediaPreviews(attachments, sensitive, listener, status.isShowingContent(), statusDisplayOptions.useBlurhash()); setMediaPreviews(attachments, sensitive, listener, status.isShowingContent(), statusDisplayOptions.useBlurhash());
if (attachments.size() == 0) { if (attachments.size() == 0) {
@ -767,13 +772,13 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
} }
} }
protected static boolean hasAudioAttachment(List<Attachment> attachments) { protected static boolean hasPreviewableAttachment(List<Attachment> attachments) {
for (Attachment attachment : attachments) { for (Attachment attachment : attachments) {
if (attachment.getType() == Attachment.Type.AUDIO) { if (attachment.getType() == Attachment.Type.AUDIO || attachment.getType() == Attachment.Type.UNKNOWN) {
return true; return false;
} }
} }
return false; return true;
} }
private void setDescriptionForStatus(@NonNull StatusViewData.Concrete status, private void setDescriptionForStatus(@NonNull StatusViewData.Concrete status,
@ -926,12 +931,28 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
listener.onViewThread(position); listener.onViewThread(position);
} }
}; };
pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), poll.getVotersCount(), emojis, PollAdapter.RESULT, viewThreadListener); pollAdapter.setup(
poll.getOptions(),
poll.getVotesCount(),
poll.getVotersCount(),
emojis,
PollAdapter.RESULT,
viewThreadListener,
statusDisplayOptions.animateEmojis()
);
pollButton.setVisibility(View.GONE); pollButton.setVisibility(View.GONE);
} else { } else {
// voting possible // voting possible
pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), poll.getVotersCount(), emojis, poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE, null); pollAdapter.setup(
poll.getOptions(),
poll.getVotesCount(),
poll.getVotersCount(),
emojis,
poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE,
null,
statusDisplayOptions.animateEmojis()
);
pollButton.setVisibility(View.VISIBLE); pollButton.setVisibility(View.VISIBLE);

View file

@ -108,7 +108,12 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
super.setupWithStatus(status, listener, statusDisplayOptions, payloads); super.setupWithStatus(status, listener, statusDisplayOptions, payloads);
setupCard(status, CardViewMode.FULL_WIDTH, statusDisplayOptions); // Always show card for detailed status setupCard(status, CardViewMode.FULL_WIDTH, statusDisplayOptions); // Always show card for detailed status
if (payloads == null) { if (payloads == null) {
setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener);
if (!statusDisplayOptions.hideStats()) {
setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener);
} else {
hideQuantitativeStats();
}
setApplication(status.getApplication()); setApplication(status.getApplication());
@ -174,4 +179,10 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
null null
); );
} }
private void hideQuantitativeStats() {
reblogs.setVisibility(View.GONE);
favourites.setVisibility(View.GONE);
infoDivider.setVisibility(View.GONE);
}
} }

View file

@ -27,8 +27,10 @@ import androidx.recyclerview.widget.RecyclerView;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.SmartLengthInputFilter; import com.keylesspalace.tusky.util.SmartLengthInputFilter;
import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.util.StringUtils;
import com.keylesspalace.tusky.viewdata.StatusViewData; import com.keylesspalace.tusky.viewdata.StatusViewData;
import at.connyduck.sparkbutton.helpers.Utils; import at.connyduck.sparkbutton.helpers.Utils;
@ -64,7 +66,7 @@ public class StatusViewHolder extends StatusBaseViewHolder {
if (rebloggedByDisplayName == null) { if (rebloggedByDisplayName == null) {
hideStatusInfo(); hideStatusInfo();
} else { } else {
setRebloggedByDisplayName(rebloggedByDisplayName); setRebloggedByDisplayName(rebloggedByDisplayName, status, statusDisplayOptions);
statusInfo.setOnClickListener(v -> listener.onOpenReblog(getAdapterPosition())); statusInfo.setOnClickListener(v -> listener.onOpenReblog(getAdapterPosition()));
} }
@ -73,10 +75,16 @@ public class StatusViewHolder extends StatusBaseViewHolder {
} }
private void setRebloggedByDisplayName(final String name) { private void setRebloggedByDisplayName(final CharSequence name,
final StatusViewData.Concrete status,
final StatusDisplayOptions statusDisplayOptions) {
Context context = statusInfo.getContext(); Context context = statusInfo.getContext();
String boostedText = context.getString(R.string.status_boosted_format, name); CharSequence wrappedName = StringUtils.unicodeWrap(name);
statusInfo.setText(boostedText); CharSequence boostedText = context.getString(R.string.status_boosted_format, wrappedName);
CharSequence emojifiedText = CustomEmojiHelper.emojify(
boostedText, status.getRebloggedByAccountEmojis(), statusInfo, statusDisplayOptions.animateEmojis()
);
statusInfo.setText(emojifiedText);
statusInfo.setVisibility(View.VISIBLE); statusInfo.setVisibility(View.VISIBLE);
} }

View file

@ -65,7 +65,9 @@ public final class TimelineAdapter extends RecyclerView.Adapter {
statusDisplayOptions.showBotOverlay(), statusDisplayOptions.showBotOverlay(),
statusDisplayOptions.useBlurhash(), statusDisplayOptions.useBlurhash(),
statusDisplayOptions.cardViewMode(), statusDisplayOptions.cardViewMode(),
statusDisplayOptions.confirmReblogs() statusDisplayOptions.confirmReblogs(),
statusDisplayOptions.hideStats(),
statusDisplayOptions.animateEmojis()
); );
} }

View file

@ -32,6 +32,7 @@ import com.keylesspalace.tusky.util.LinkHelper
import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.emojify
import kotlinx.android.synthetic.main.item_announcement.view.* import kotlinx.android.synthetic.main.item_announcement.view.*
interface AnnouncementActionListener: LinkListener { interface AnnouncementActionListener: LinkListener {
fun openReactionPicker(announcementId: String, target: View) fun openReactionPicker(announcementId: String, target: View)
fun addReaction(announcementId: String, name: String) fun addReaction(announcementId: String, name: String)
@ -40,7 +41,9 @@ interface AnnouncementActionListener: LinkListener {
class AnnouncementAdapter( class AnnouncementAdapter(
private var items: List<Announcement> = emptyList(), private var items: List<Announcement> = emptyList(),
private val listener: AnnouncementActionListener private val listener: AnnouncementActionListener,
private val wellbeingEnabled: Boolean = false,
private val animateEmojis: Boolean = false
) : RecyclerView.Adapter<AnnouncementAdapter.AnnouncementViewHolder>() { ) : RecyclerView.Adapter<AnnouncementAdapter.AnnouncementViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnnouncementViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnnouncementViewHolder {
@ -68,6 +71,14 @@ class AnnouncementAdapter(
fun bind(item: Announcement) { fun bind(item: Announcement) {
LinkHelper.setClickableText(text, item.content, null, listener) LinkHelper.setClickableText(text, item.content, null, listener)
// If wellbeing mode is enabled, announcement badge counts should not be shown.
if (wellbeingEnabled) {
// Since reactions are not visible in wellbeing mode,
// we shouldn't be able to add any ourselves.
addReactionChip.visibility = View.GONE
return
}
item.reactions.forEachIndexed { i, reaction -> item.reactions.forEachIndexed { i, reaction ->
(chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip? (chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip?
?: Chip(ContextThemeWrapper(view.context, R.style.Widget_MaterialComponents_Chip_Choice)).apply { ?: Chip(ContextThemeWrapper(view.context, R.style.Widget_MaterialComponents_Chip_Choice)).apply {
@ -89,7 +100,8 @@ class AnnouncementAdapter(
reaction.staticUrl ?: "", reaction.staticUrl ?: "",
null null
)), )),
this this,
animateEmojis
) )
isChecked = reaction.me isChecked = reaction.me

View file

@ -17,18 +17,22 @@ package com.keylesspalace.tusky.components.announcements
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.PopupWindow import android.widget.PopupWindow
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.keylesspalace.tusky.* import com.keylesspalace.tusky.BottomSheetActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.ViewTagActivity
import com.keylesspalace.tusky.adapter.EmojiAdapter import com.keylesspalace.tusky.adapter.EmojiAdapter
import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.view.EmojiPicker import com.keylesspalace.tusky.view.EmojiPicker
import kotlinx.android.synthetic.main.activity_announcements.* import kotlinx.android.synthetic.main.activity_announcements.*
@ -42,7 +46,7 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener,
private val viewModel: AnnouncementsViewModel by viewModels { viewModelFactory } private val viewModel: AnnouncementsViewModel by viewModels { viewModelFactory }
private val adapter = AnnouncementAdapter(emptyList(), this) private lateinit var adapter: AnnouncementAdapter
private val picker by lazy { EmojiPicker(this) } private val picker by lazy { EmojiPicker(this) }
private val pickerDialog by lazy { private val pickerDialog by lazy {
@ -75,6 +79,13 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener,
announcementsList.layoutManager = LinearLayoutManager(this) announcementsList.layoutManager = LinearLayoutManager(this)
val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL) val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
announcementsList.addItemDecoration(divider) announcementsList.addItemDecoration(divider)
val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false)
val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
adapter = AnnouncementAdapter(emptyList(), this, wellbeingEnabled, animateEmojis)
announcementsList.adapter = adapter announcementsList.adapter = adapter
viewModel.announcements.observe(this) { viewModel.announcements.observe(this) {
@ -112,16 +123,6 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener,
progressBar.show() progressBar.show()
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun refreshAnnouncements() { private fun refreshAnnouncements() {
viewModel.load() viewModel.load()
swipeRefreshLayout.isRefreshing = true swipeRefreshLayout.isRefreshing = true

View file

@ -30,7 +30,6 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.provider.MediaStore import android.provider.MediaStore
import android.text.TextUtils
import android.util.Log import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MenuItem import android.view.MenuItem
@ -57,19 +56,20 @@ import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.BuildConfig
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.adapter.ComposeAutoCompleteAdapter
import com.keylesspalace.tusky.adapter.EmojiAdapter import com.keylesspalace.tusky.adapter.EmojiAdapter
import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener
import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog
import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog
import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener
import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.DraftAttachment
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.NewPoll import com.keylesspalace.tusky.entity.NewPoll
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.*
import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
@ -81,7 +81,6 @@ import java.io.File
import java.io.IOException import java.io.IOException
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
import kotlin.collections.ArrayList
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@ -104,12 +103,13 @@ class ComposeActivity : BaseActivity(),
// this only exists when a status is trying to be sent, but uploads are still occurring // this only exists when a status is trying to be sent, but uploads are still occurring
private var finishingUploadDialog: ProgressDialog? = null private var finishingUploadDialog: ProgressDialog? = null
private var photoUploadUri: Uri? = null private var photoUploadUri: Uri? = null
@VisibleForTesting @VisibleForTesting
var maximumTootCharacters = DEFAULT_CHARACTER_LIMIT var maximumTootCharacters = DEFAULT_CHARACTER_LIMIT
private var composeOptions: ComposeOptions? = null
private val viewModel: ComposeViewModel by viewModels { viewModelFactory } private val viewModel: ComposeViewModel by viewModels { viewModelFactory }
private val maxUploadMediaNumber = 4
private var mediaCount = 0 private var mediaCount = 0
public override fun onCreate(savedInstanceState: Bundle?) { public override fun onCreate(savedInstanceState: Bundle?) {
@ -147,59 +147,45 @@ class ComposeActivity : BaseActivity(),
/* If the composer is started up as a reply to another post, override the "starting" state /* If the composer is started up as a reply to another post, override the "starting" state
* based on what the intent from the reply request passes. */ * based on what the intent from the reply request passes. */
if (intent != null) {
this.composeOptions = intent.getParcelableExtra(COMPOSE_OPTIONS_EXTRA) val composeOptions: ComposeOptions? = intent.getParcelableExtra(COMPOSE_OPTIONS_EXTRA)
viewModel.setup(composeOptions)
setupReplyViews(composeOptions?.replyingStatusAuthor) viewModel.setup(composeOptions)
val tootText = composeOptions?.tootText setupReplyViews(composeOptions?.replyingStatusAuthor, composeOptions?.replyingStatusContent)
if (!tootText.isNullOrEmpty()) { val tootText = composeOptions?.tootText
composeEditField.setText(tootText) if (!tootText.isNullOrEmpty()) {
} composeEditField.setText(tootText)
} }
if (!TextUtils.isEmpty(composeOptions?.scheduledAt)) { if (!composeOptions?.scheduledAt.isNullOrEmpty()) {
composeScheduleView.setDateTime(composeOptions?.scheduledAt) composeScheduleView.setDateTime(composeOptions?.scheduledAt)
} }
setupComposeField(viewModel.startingText) setupComposeField(preferences, viewModel.startingText)
setupContentWarningField(composeOptions?.contentWarning) setupContentWarningField(composeOptions?.contentWarning)
setupPollView() setupPollView()
applyShareIntent(intent, savedInstanceState) applyShareIntent(intent, savedInstanceState)
viewModel.setupComplete.value = true viewModel.setupComplete.value = true
} }
private fun applyShareIntent(intent: Intent?, savedInstanceState: Bundle?) { private fun applyShareIntent(intent: Intent, savedInstanceState: Bundle?) {
if (intent != null && savedInstanceState == null) { if (savedInstanceState == null) {
/* Get incoming images being sent through a share action from another app. Only do this /* Get incoming images being sent through a share action from another app. Only do this
* when savedInstanceState is null, otherwise both the images from the intent and the * when savedInstanceState is null, otherwise both the images from the intent and the
* instance state will be re-queued. */ * instance state will be re-queued. */
val type = intent.type intent.type?.also { type ->
if (type != null) {
if (type.startsWith("image/") || type.startsWith("video/") || type.startsWith("audio/")) { if (type.startsWith("image/") || type.startsWith("video/") || type.startsWith("audio/")) {
val uriList = ArrayList<Uri>() when (intent.action) {
if (intent.action != null) { Intent.ACTION_SEND -> {
when (intent.action) { intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)?.let { uri ->
Intent.ACTION_SEND -> { pickMedia(uri)
val uri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM) }
if (uri != null) { }
uriList.add(uri) Intent.ACTION_SEND_MULTIPLE -> {
} intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)?.forEach { uri ->
} pickMedia(uri)
Intent.ACTION_SEND_MULTIPLE -> {
val list = intent.getParcelableArrayListExtra<Uri>(
Intent.EXTRA_STREAM)
if (list != null) {
for (uri in list) {
if (uri != null) {
uriList.add(uri)
}
}
}
} }
} }
}
for (uri in uriList) {
pickMedia(uri)
} }
} else if (type == "text/plain" && intent.action == Intent.ACTION_SEND) { } else if (type == "text/plain" && intent.action == Intent.ACTION_SEND) {
@ -217,13 +203,16 @@ class ComposeActivity : BaseActivity(),
val left = min(start, end) val left = min(start, end)
val right = max(start, end) val right = max(start, end)
composeEditField.text.replace(left, right, shareBody, 0, shareBody.length) composeEditField.text.replace(left, right, shareBody, 0, shareBody.length)
// move edittext cursor to first when shareBody parsed
composeEditField.text.insert(0, "\n")
composeEditField.setSelection(0)
} }
} }
} }
} }
} }
private fun setupReplyViews(replyingStatusAuthor: String?) { private fun setupReplyViews(replyingStatusAuthor: String?, replyingStatusContent: String?) {
if (replyingStatusAuthor != null) { if (replyingStatusAuthor != null) {
composeReplyView.show() composeReplyView.show()
composeReplyView.text = getString(R.string.replying_to, replyingStatusAuthor) composeReplyView.text = getString(R.string.replying_to, replyingStatusAuthor)
@ -247,7 +236,7 @@ class ComposeActivity : BaseActivity(),
} }
} }
} }
composeOptions?.replyingStatusContent?.let { composeReplyContentView.text = it } replyingStatusContent?.let { composeReplyContentView.text = it }
} }
private fun setupContentWarningField(startingContentWarning: String?) { private fun setupContentWarningField(startingContentWarning: String?) {
@ -257,13 +246,18 @@ class ComposeActivity : BaseActivity(),
composeContentWarningField.onTextChanged { _, _, _, _ -> updateVisibleCharactersLeft() } composeContentWarningField.onTextChanged { _, _, _, _ -> updateVisibleCharactersLeft() }
} }
private fun setupComposeField(startingText: String?) { private fun setupComposeField(preferences: SharedPreferences, startingText: String?) {
composeEditField.setOnCommitContentListener(this) composeEditField.setOnCommitContentListener(this)
composeEditField.setOnKeyListener { _, keyCode, event -> this.onKeyDown(keyCode, event) } composeEditField.setOnKeyListener { _, keyCode, event -> this.onKeyDown(keyCode, event) }
composeEditField.setAdapter( composeEditField.setAdapter(
ComposeAutoCompleteAdapter(this)) ComposeAutoCompleteAdapter(
this,
preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
)
)
composeEditField.setTokenizer(ComposeTokenizer()) composeEditField.setTokenizer(ComposeTokenizer())
composeEditField.setText(startingText) composeEditField.setText(startingText)
@ -650,7 +644,6 @@ class ComposeActivity : BaseActivity(),
} }
} }
private fun removePoll() { private fun removePoll() {
viewModel.poll.value = null viewModel.poll.value = null
pollPreview.hide() pollPreview.hide()
@ -807,6 +800,7 @@ class ComposeActivity : BaseActivity(),
val mimeTypes = arrayOf("image/*", "video/*", "audio/*") val mimeTypes = arrayOf("image/*", "video/*", "audio/*")
intent.type = "*/*" intent.type = "*/*"
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes) intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
startActivityForResult(intent, MEDIA_PICK_RESULT) startActivityForResult(intent, MEDIA_PICK_RESULT)
} }
@ -833,7 +827,23 @@ class ComposeActivity : BaseActivity(),
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
super.onActivityResult(requestCode, resultCode, intent) super.onActivityResult(requestCode, resultCode, intent)
if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_PICK_RESULT && intent != null) { if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_PICK_RESULT && intent != null) {
pickMedia(intent.data!!) if (intent.data != null) {
// Single media, upload it and done.
pickMedia(intent.data!!)
} else if (intent.clipData != null) {
val clipData = intent.clipData!!
val count = clipData.itemCount
if (mediaCount + count > maxUploadMediaNumber) {
// check if exist media + upcoming media > 4, then prob error message.
Toast.makeText(this, getString(R.string.error_upload_max_media_reached, maxUploadMediaNumber), Toast.LENGTH_SHORT).show()
} else {
// if not grater then 4, upload all multiple media.
for (i in 0 until count) {
val imageUri = clipData.getItemAt(i).getUri()
pickMedia(imageUri)
}
}
}
} else if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_TAKE_PHOTO_RESULT) { } else if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_TAKE_PHOTO_RESULT) {
pickMedia(photoUploadUri!!) pickMedia(photoUploadUri!!)
} }
@ -1000,8 +1010,9 @@ class ComposeActivity : BaseActivity(),
@Parcelize @Parcelize
data class ComposeOptions( data class ComposeOptions(
// Let's keep fields var until all consumers are Kotlin // Let's keep fields var until all consumers are Kotlin
var scheduledTootUid: String? = null, var scheduledTootId: String? = null,
var savedTootUid: Int? = null, var savedTootUid: Int? = null,
var draftId: Int? = null,
var tootText: String? = null, var tootText: String? = null,
var mediaUrls: List<String>? = null, var mediaUrls: List<String>? = null,
var mediaDescriptions: List<String>? = null, var mediaDescriptions: List<String>? = null,
@ -1013,6 +1024,7 @@ class ComposeActivity : BaseActivity(),
var replyingStatusAuthor: String? = null, var replyingStatusAuthor: String? = null,
var replyingStatusContent: String? = null, var replyingStatusContent: String? = null,
var mediaAttachments: List<Attachment>? = null, var mediaAttachments: List<Attachment>? = null,
var draftAttachments: List<DraftAttachment>? = null,
var scheduledAt: String? = null, var scheduledAt: String? = null,
var sensitive: Boolean? = null, var sensitive: Boolean? = null,
var poll: NewPoll? = null, var poll: NewPoll? = null,
@ -1039,7 +1051,6 @@ class ComposeActivity : BaseActivity(),
} }
} }
@JvmStatic
fun canHandleMimeType(mimeType: String?): Boolean { fun canHandleMimeType(mimeType: String?): Boolean {
return mimeType != null && (mimeType.startsWith("image/") || mimeType.startsWith("video/") || mimeType.startsWith("audio/") || mimeType == "text/plain") return mimeType != null && (mimeType.startsWith("image/") || mimeType.startsWith("video/") || mimeType.startsWith("audio/") || mimeType == "text/plain")
} }

View file

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not, * You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */ * see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.adapter; package com.keylesspalace.tusky.components.compose;
import android.content.Context; import android.content.Context;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
@ -53,11 +53,15 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter
private final ArrayList<AutocompleteResult> resultList; private final ArrayList<AutocompleteResult> resultList;
private final AutocompletionProvider autocompletionProvider; private final AutocompletionProvider autocompletionProvider;
private final boolean animateAvatar;
private final boolean animateEmojis;
public ComposeAutoCompleteAdapter(AutocompletionProvider autocompletionProvider) { public ComposeAutoCompleteAdapter(AutocompletionProvider autocompletionProvider, boolean animateAvatar, boolean animateEmojis) {
super(); super();
resultList = new ArrayList<>(); resultList = new ArrayList<>();
this.autocompletionProvider = autocompletionProvider; this.autocompletionProvider = autocompletionProvider;
this.animateAvatar = animateAvatar;
this.animateEmojis = animateEmojis;
} }
@Override @Override
@ -147,15 +151,12 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter
); );
accountViewHolder.username.setText(formattedUsername); accountViewHolder.username.setText(formattedUsername);
CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(),
account.getEmojis(), accountViewHolder.displayName); account.getEmojis(), accountViewHolder.displayName, animateEmojis);
accountViewHolder.displayName.setText(emojifiedName); accountViewHolder.displayName.setText(emojifiedName);
int avatarRadius = accountViewHolder.avatar.getContext().getResources() int avatarRadius = accountViewHolder.avatar.getContext().getResources()
.getDimensionPixelSize(R.dimen.avatar_radius_42dp); .getDimensionPixelSize(R.dimen.avatar_radius_42dp);
boolean animateAvatar = PreferenceManager.getDefaultSharedPreferences(accountViewHolder.avatar.getContext())
.getBoolean("animateGifAvatars", false);
ImageLoadingHelper.loadAvatar( ImageLoadingHelper.loadAvatar(
account.getAvatar(), account.getAvatar(),
accountViewHolder.avatar, accountViewHolder.avatar,

View file

@ -21,8 +21,8 @@ import androidx.core.net.toUri
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.keylesspalace.tusky.adapter.ComposeAutoCompleteAdapter
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
import com.keylesspalace.tusky.components.drafts.DraftHelper
import com.keylesspalace.tusky.components.search.SearchType import com.keylesspalace.tusky.components.search.SearchType
import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.AppDatabase
@ -39,18 +39,12 @@ import io.reactivex.rxkotlin.Singles
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
/** class ComposeViewModel @Inject constructor(
* Throw when trying to add an image when video is already present or the other way around
*/
class VideoOrImageException : Exception()
class ComposeViewModel
@Inject constructor(
private val api: MastodonApi, private val api: MastodonApi,
private val accountManager: AccountManager, private val accountManager: AccountManager,
private val mediaUploader: MediaUploader, private val mediaUploader: MediaUploader,
private val serviceClient: ServiceClient, private val serviceClient: ServiceClient,
private val draftHelper: DraftHelper,
private val saveTootHelper: SaveTootHelper, private val saveTootHelper: SaveTootHelper,
private val db: AppDatabase private val db: AppDatabase
) : RxAwareViewModel() { ) : RxAwareViewModel() {
@ -59,7 +53,8 @@ class ComposeViewModel
private var replyingStatusContent: String? = null private var replyingStatusContent: String? = null
internal var startingText: String? = null internal var startingText: String? = null
private var savedTootUid: Int = 0 private var savedTootUid: Int = 0
private var scheduledTootUid: String? = null private var draftId: Int = 0
private var scheduledTootId: String? = null
private var startingContentWarning: String = "" private var startingContentWarning: String = ""
private var inReplyToId: String? = null private var inReplyToId: String? = null
private var startingVisibility: Status.Visibility = Status.Visibility.UNKNOWN private var startingVisibility: Status.Visibility = Status.Visibility.UNKNOWN
@ -81,10 +76,6 @@ class ComposeViewModel
val markMediaAsSensitive = val markMediaAsSensitive =
mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false) mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
fun toggleMarkSensitive() {
this.markMediaAsSensitive.value = !this.markMediaAsSensitive.value!!
}
val statusVisibility = mutableLiveData(Status.Visibility.UNKNOWN) val statusVisibility = mutableLiveData(Status.Visibility.UNKNOWN)
val showContentWarning = mutableLiveData(false) val showContentWarning = mutableLiveData(false)
val setupComplete = mutableLiveData(false) val setupComplete = mutableLiveData(false)
@ -96,7 +87,7 @@ class ComposeViewModel
private val mediaToDisposable = mutableMapOf<Long, Disposable>() private val mediaToDisposable = mutableMapOf<Long, Disposable>()
private val isEditingScheduledToot get() = !scheduledTootUid.isNullOrEmpty() private val isEditingScheduledToot get() = !scheduledTootId.isNullOrEmpty()
init { init {
@ -116,7 +107,7 @@ class ComposeViewModel
.onErrorResumeNext( .onErrorResumeNext(
db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!) db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
) )
.subscribe ({ instanceEntity -> .subscribe({ instanceEntity ->
emoji.postValue(instanceEntity.emojiList) emoji.postValue(instanceEntity.emojiList)
instance.postValue(instanceEntity) instance.postValue(instanceEntity)
}, { throwable -> }, { throwable ->
@ -126,7 +117,7 @@ class ComposeViewModel
.autoDispose() .autoDispose()
} }
fun pickMedia(uri: Uri): LiveData<Either<Throwable, QueuedMedia>> { fun pickMedia(uri: Uri, description: String? = null): LiveData<Either<Throwable, QueuedMedia>> {
// We are not calling .toLiveData() here because we don't want to stop the process when // We are not calling .toLiveData() here because we don't want to stop the process when
// the Activity goes away temporarily (like on screen rotation). // the Activity goes away temporarily (like on screen rotation).
val liveData = MutableLiveData<Either<Throwable, QueuedMedia>>() val liveData = MutableLiveData<Either<Throwable, QueuedMedia>>()
@ -138,7 +129,7 @@ class ComposeViewModel
&& mediaItems[0].type == QueuedMedia.Type.IMAGE) { && mediaItems[0].type == QueuedMedia.Type.IMAGE) {
throw VideoOrImageException() throw VideoOrImageException()
} else { } else {
addMediaToQueue(type, uri, size) addMediaToQueue(type, uri, size, description)
} }
} }
.subscribe({ queuedMedia -> .subscribe({ queuedMedia ->
@ -150,12 +141,23 @@ class ComposeViewModel
return liveData return liveData
} }
private fun addMediaToQueue(type: QueuedMedia.Type, uri: Uri, mediaSize: Long): QueuedMedia { private fun addMediaToQueue(
val mediaItem = QueuedMedia(System.currentTimeMillis(), uri, type, mediaSize) type: QueuedMedia.Type,
uri: Uri,
mediaSize: Long,
description: String? = null
): QueuedMedia {
val mediaItem = QueuedMedia(
localId = System.currentTimeMillis(),
uri = uri,
type = type,
mediaSize = mediaSize,
description = description
)
media.value = media.value!! + mediaItem media.value = media.value!! + mediaItem
mediaToDisposable[mediaItem.localId] = mediaUploader mediaToDisposable[mediaItem.localId] = mediaUploader
.uploadMedia(mediaItem) .uploadMedia(mediaItem)
.subscribe ({ event -> .subscribe({ event ->
val item = media.value?.find { it.localId == mediaItem.localId } val item = media.value?.find { it.localId == mediaItem.localId }
?: return@subscribe ?: return@subscribe
val newMediaItem = when (event) { val newMediaItem = when (event) {
@ -190,6 +192,10 @@ class ComposeViewModel
media.value = media.value!!.withoutFirstWhich { it.localId == item.localId } media.value = media.value!!.withoutFirstWhich { it.localId == item.localId }
} }
fun toggleMarkSensitive() {
this.markMediaAsSensitive.value = this.markMediaAsSensitive.value != true
}
fun didChange(content: String?, contentWarning: String?): Boolean { fun didChange(content: String?, contentWarning: String?): Boolean {
val textChanged = !(content.isNullOrEmpty() val textChanged = !(content.isNullOrEmpty()
@ -210,29 +216,37 @@ class ComposeViewModel
} }
fun deleteDraft() { fun deleteDraft() {
saveTootHelper.deleteDraft(this.savedTootUid) if (savedTootUid != 0) {
saveTootHelper.deleteDraft(savedTootUid)
}
if (draftId != 0) {
draftHelper.deleteDraftAndAttachments(draftId)
.subscribe()
}
} }
fun saveDraft(content: String, contentWarning: String) { fun saveDraft(content: String, contentWarning: String) {
val mediaUris = mutableListOf<String>()
val mediaDescriptions = mutableListOf<String?>() val mediaUris: MutableList<String> = mutableListOf()
for (item in media.value!!) { val mediaDescriptions: MutableList<String?> = mutableListOf()
media.value?.forEach { item ->
mediaUris.add(item.uri.toString()) mediaUris.add(item.uri.toString())
mediaDescriptions.add(item.description) mediaDescriptions.add(item.description)
} }
saveTootHelper.saveToot(
content, draftHelper.saveDraft(
contentWarning, draftId = draftId,
null, accountId = accountManager.activeAccount?.id!!,
mediaUris, inReplyToId = inReplyToId,
mediaDescriptions, content = content,
savedTootUid, contentWarning = contentWarning,
inReplyToId, sensitive = markMediaAsSensitive.value!!,
replyingStatusContent, visibility = statusVisibility.value!!,
replyingStatusAuthor, mediaUris = mediaUris,
statusVisibility.value!!, mediaDescriptions = mediaDescriptions,
poll.value poll = poll.value,
) failedToSend = false
).subscribe()
} }
/** /**
@ -246,7 +260,7 @@ class ComposeViewModel
): LiveData<Unit> { ): LiveData<Unit> {
val deletionObservable = if (isEditingScheduledToot) { val deletionObservable = if (isEditingScheduledToot) {
api.deleteScheduledStatus(scheduledTootUid.toString()).toObservable().map { Unit } api.deleteScheduledStatus(scheduledTootId.toString()).toObservable().map { }
} else { } else {
just(Unit) just(Unit)
}.toLiveData() }.toLiveData()
@ -264,21 +278,21 @@ class ComposeViewModel
} }
val tootToSend = TootToSend( val tootToSend = TootToSend(
content, text = content,
spoilerText, warningText = spoilerText,
statusVisibility.value!!.serverString(), visibility = statusVisibility.value!!.serverString(),
mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!), sensitive = mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!),
mediaIds, mediaIds = mediaIds,
mediaUris.map { it.toString() }, mediaUris = mediaUris.map { it.toString() },
mediaDescriptions, mediaDescriptions = mediaDescriptions,
scheduledAt = scheduledAt.value, scheduledAt = scheduledAt.value,
inReplyToId = inReplyToId, inReplyToId = inReplyToId,
poll = poll.value, poll = poll.value,
replyingStatusContent = null, replyingStatusContent = null,
replyingStatusAuthorUsername = null, replyingStatusAuthorUsername = null,
savedJsonUrls = null,
accountId = accountManager.activeAccount!!.id, accountId = accountManager.activeAccount!!.id,
savedTootUid = 0, savedTootUid = savedTootUid,
draftId = draftId,
idempotencyKey = randomAlphanumericString(16), idempotencyKey = randomAlphanumericString(16),
retries = 0 retries = 0
) )
@ -286,9 +300,7 @@ class ComposeViewModel
serviceClient.sendToot(tootToSend) serviceClient.sendToot(tootToSend)
} }
return combineLiveData(deletionObservable, sendObservable) { _, _ -> Unit } return combineLiveData(deletionObservable, sendObservable) { _, _ -> }
} }
fun updateDescription(localId: Long, description: String): LiveData<Boolean> { fun updateDescription(localId: Long, description: String): LiveData<Boolean> {
@ -319,7 +331,6 @@ class ComposeViewModel
return completedCaptioningLiveData return completedCaptioningLiveData
} }
fun searchAutocompleteSuggestions(token: String): List<ComposeAutoCompleteAdapter.AutocompleteResult> { fun searchAutocompleteSuggestions(token: String): List<ComposeAutoCompleteAdapter.AutocompleteResult> {
when (token[0]) { when (token[0]) {
'@' -> { '@' -> {
@ -370,14 +381,12 @@ class ComposeViewModel
} }
} }
override fun onCleared() {
for (uploadDisposable in mediaToDisposable.values) {
uploadDisposable.dispose()
}
super.onCleared()
}
fun setup(composeOptions: ComposeActivity.ComposeOptions?) { fun setup(composeOptions: ComposeActivity.ComposeOptions?) {
if (setupComplete.value == true) {
return
}
val preferredVisibility = accountManager.activeAccount!!.defaultPostPrivacy val preferredVisibility = accountManager.activeAccount!!.defaultPostPrivacy
val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN
@ -385,6 +394,7 @@ class ComposeViewModel
preferredVisibility.num.coerceAtLeast(replyVisibility.num)) preferredVisibility.num.coerceAtLeast(replyVisibility.num))
inReplyToId = composeOptions?.inReplyToId inReplyToId = composeOptions?.inReplyToId
modifiedInitialState = composeOptions?.modifiedInitialState == true modifiedInitialState = composeOptions?.modifiedInitialState == true
val contentWarning = composeOptions?.contentWarning val contentWarning = composeOptions?.contentWarning
@ -396,10 +406,11 @@ class ComposeViewModel
} }
// recreate media list // recreate media list
// when coming from SavedTootActivity
val loadedDraftMediaUris = composeOptions?.mediaUrls val loadedDraftMediaUris = composeOptions?.mediaUrls
val loadedDraftMediaDescriptions: List<String?>? = composeOptions?.mediaDescriptions val loadedDraftMediaDescriptions: List<String?>? = composeOptions?.mediaDescriptions
val draftAttachments = composeOptions?.draftAttachments
if (loadedDraftMediaUris != null && loadedDraftMediaDescriptions != null) { if (loadedDraftMediaUris != null && loadedDraftMediaDescriptions != null) {
// when coming from SavedTootActivity
loadedDraftMediaUris.zip(loadedDraftMediaDescriptions) loadedDraftMediaUris.zip(loadedDraftMediaDescriptions)
.forEach { (uri, description) -> .forEach { (uri, description) ->
pickMedia(uri.toUri()).observeForever { errorOrItem -> pickMedia(uri.toUri()).observeForever { errorOrItem ->
@ -408,23 +419,24 @@ class ComposeViewModel
} }
} }
} }
} else if (draftAttachments != null) {
// when coming from DraftActivity
draftAttachments.forEach { attachment -> pickMedia(attachment.uri, attachment.description) }
} else composeOptions?.mediaAttachments?.forEach { a -> } else composeOptions?.mediaAttachments?.forEach { a ->
// when coming from redraft // when coming from redraft or ScheduledTootActivity
val mediaType = when (a.type) { val mediaType = when (a.type) {
Attachment.Type.VIDEO, Attachment.Type.GIFV -> QueuedMedia.Type.VIDEO Attachment.Type.VIDEO, Attachment.Type.GIFV -> QueuedMedia.Type.VIDEO
Attachment.Type.UNKNOWN, Attachment.Type.IMAGE -> QueuedMedia.Type.IMAGE Attachment.Type.UNKNOWN, Attachment.Type.IMAGE -> QueuedMedia.Type.IMAGE
Attachment.Type.AUDIO -> QueuedMedia.Type.AUDIO Attachment.Type.AUDIO -> QueuedMedia.Type.AUDIO
else -> QueuedMedia.Type.IMAGE
} }
addUploadedMedia(a.id, mediaType, a.url.toUri(), a.description) addUploadedMedia(a.id, mediaType, a.url.toUri(), a.description)
} }
savedTootUid = composeOptions?.savedTootUid ?: 0 savedTootUid = composeOptions?.savedTootUid ?: 0
scheduledTootUid = composeOptions?.scheduledTootUid draftId = composeOptions?.draftId ?: 0
scheduledTootId = composeOptions?.scheduledTootId
startingText = composeOptions?.tootText startingText = composeOptions?.tootText
val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN
if (tootVisibility.num != Status.Visibility.UNKNOWN.num) { if (tootVisibility.num != Status.Visibility.UNKNOWN.num) {
startingVisibility = tootVisibility startingVisibility = tootVisibility
@ -441,7 +453,6 @@ class ComposeViewModel
startingText = builder.toString() startingText = builder.toString()
} }
scheduledAt.value = composeOptions?.scheduledAt scheduledAt.value = composeOptions?.scheduledAt
composeOptions?.sensitive?.let { markMediaAsSensitive.value = it } composeOptions?.sensitive?.let { markMediaAsSensitive.value = it }
@ -462,6 +473,13 @@ class ComposeViewModel
scheduledAt.value = newScheduledAt scheduledAt.value = newScheduledAt
} }
override fun onCleared() {
for (uploadDisposable in mediaToDisposable.values) {
uploadDisposable.dispose()
}
super.onCleared()
}
private companion object { private companion object {
const val TAG = "ComposeViewModel" const val TAG = "ComposeViewModel"
} }
@ -479,4 +497,9 @@ data class ComposeInstanceParams(
val pollMaxOptions: Int, val pollMaxOptions: Int,
val pollMaxLength: Int, val pollMaxLength: Int,
val supportsScheduled: Boolean val supportsScheduled: Boolean
) )
/**
* Thrown when trying to add an image when video is already present or the other way around
*/
class VideoOrImageException : Exception()

View file

@ -173,7 +173,13 @@ class MediaUploaderImpl(
val body = MultipartBody.Part.createFormData("file", filename, fileBody) val body = MultipartBody.Part.createFormData("file", filename, fileBody)
val uploadDisposable = mastodonApi.uploadMedia(body) val description = if (media.description != null) {
MultipartBody.Part.createFormData("description", media.description)
} else {
null
}
val uploadDisposable = mastodonApi.uploadMedia(body, description)
.subscribe({ attachment -> .subscribe({ attachment ->
emitter.onNext(UploadEvent.FinishedEvent(attachment)) emitter.onNext(UploadEvent.FinishedEvent(attachment))
emitter.onComplete() emitter.onComplete()

View file

@ -75,7 +75,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
setupCollapsedState(status.getCollapsible(), status.getCollapsed(), status.getExpanded(), status.getSpoilerText(), listener); setupCollapsedState(status.getCollapsible(), status.getCollapsed(), status.getExpanded(), status.getSpoilerText(), listener);
setDisplayName(account.getDisplayName(), account.getEmojis()); setDisplayName(account.getDisplayName(), account.getEmojis(), statusDisplayOptions);
setUsername(account.getUsername()); setUsername(account.getUsername());
setCreatedAt(status.getCreatedAt(), statusDisplayOptions); setCreatedAt(status.getCreatedAt(), statusDisplayOptions);
setIsReply(status.getInReplyToId() != null); setIsReply(status.getInReplyToId() != null);
@ -83,7 +83,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
setBookmarked(status.getBookmarked()); setBookmarked(status.getBookmarked());
List<Attachment> attachments = status.getAttachments(); List<Attachment> attachments = status.getAttachments();
boolean sensitive = status.getSensitive(); boolean sensitive = status.getSensitive();
if (statusDisplayOptions.mediaPreviewEnabled() && !hasAudioAttachment(attachments)) { if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) {
setMediaPreviews(attachments, sensitive, listener, status.getShowingHiddenContent(), setMediaPreviews(attachments, sensitive, listener, status.getShowingHiddenContent(),
statusDisplayOptions.useBlurhash()); statusDisplayOptions.useBlurhash());

View file

@ -28,12 +28,12 @@ import androidx.recyclerview.widget.SimpleItemAnimator
import com.keylesspalace.tusky.AccountActivity import com.keylesspalace.tusky.AccountActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.ViewTagActivity import com.keylesspalace.tusky.ViewTagActivity
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.fragment.SFragment import com.keylesspalace.tusky.fragment.SFragment
import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.interfaces.ReselectableFragment
import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.CardViewMode import com.keylesspalace.tusky.util.CardViewMode
import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.NetworkState
import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.StatusDisplayOptions
@ -45,8 +45,6 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
@Inject
lateinit var db: AppDatabase
private val viewModel: ConversationsViewModel by viewModels { viewModelFactory } private val viewModel: ConversationsViewModel by viewModels { viewModelFactory }
@ -68,7 +66,9 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
showBotOverlay = preferences.getBoolean("showBotOverlay", true), showBotOverlay = preferences.getBoolean("showBotOverlay", true),
useBlurhash = preferences.getBoolean("useBlurhash", true), useBlurhash = preferences.getBoolean("useBlurhash", true),
cardViewMode = CardViewMode.NONE, cardViewMode = CardViewMode.NONE,
confirmReblogs = preferences.getBoolean("confirmReblogs", true) confirmReblogs = preferences.getBoolean("confirmReblogs", true),
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
) )
adapter = ConversationAdapter(statusDisplayOptions, this, ::onTopLoaded, viewModel::retry) adapter = ConversationAdapter(statusDisplayOptions, this, ::onTopLoaded, viewModel::retry)

View file

@ -0,0 +1,175 @@
/* Copyright 2021 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.components.drafts
import android.content.Context
import android.net.Uri
import android.util.Log
import android.webkit.MimeTypeMap
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import com.keylesspalace.tusky.BuildConfig
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.db.DraftAttachment
import com.keylesspalace.tusky.db.DraftEntity
import com.keylesspalace.tusky.entity.NewPoll
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.util.IOUtils
import io.reactivex.Completable
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.schedulers.Schedulers
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import javax.inject.Inject
class DraftHelper @Inject constructor(
val context: Context,
db: AppDatabase
) {
private val draftDao = db.draftDao()
fun saveDraft(
draftId: Int,
accountId: Long,
inReplyToId: String?,
content: String?,
contentWarning: String?,
sensitive: Boolean,
visibility: Status.Visibility,
mediaUris: List<String>,
mediaDescriptions: List<String?>,
poll: NewPoll?,
failedToSend: Boolean
): Completable {
return Single.fromCallable {
val externalFilesDir = context.getExternalFilesDir("Tusky")
if (externalFilesDir == null || !(externalFilesDir.exists())) {
Log.e("DraftHelper", "Error obtaining directory to save media.")
throw Exception()
}
val draftDirectory = File(externalFilesDir, "Drafts")
if (!draftDirectory.exists()) {
draftDirectory.mkdir()
}
val uris = mediaUris.map { uriString ->
uriString.toUri()
}.map { uri ->
if (uri.isNotInFolder(draftDirectory)) {
uri.copyToFolder(draftDirectory)
} else {
uri
}
}
val types = uris.map { uri ->
val mimeType = context.contentResolver.getType(uri)
when (mimeType?.substring(0, mimeType.indexOf('/'))) {
"video" -> DraftAttachment.Type.VIDEO
"image" -> DraftAttachment.Type.IMAGE
"audio" -> DraftAttachment.Type.AUDIO
else -> throw IllegalStateException("unknown media type")
}
}
val attachments: MutableList<DraftAttachment> = mutableListOf()
for (i in mediaUris.indices) {
attachments.add(
DraftAttachment(
uriString = uris[i].toString(),
description = mediaDescriptions[i],
type = types[i]
)
)
}
DraftEntity(
id = draftId,
accountId = accountId,
inReplyToId = inReplyToId,
content = content,
contentWarning = contentWarning,
sensitive = sensitive,
visibility = visibility,
attachments = attachments,
poll = poll,
failedToSend = failedToSend
)
}.flatMapCompletable { draft ->
draftDao.insertOrReplace(draft)
}.subscribeOn(Schedulers.io())
}
fun deleteDraftAndAttachments(draftId: Int): Completable {
return draftDao.find(draftId)
.flatMapCompletable { draft ->
deleteDraftAndAttachments(draft)
}
}
fun deleteDraftAndAttachments(draft: DraftEntity): Completable {
return deleteAttachments(draft)
.andThen(draftDao.delete(draft.id))
}
fun deleteAllDraftsAndAttachmentsForAccount(accountId: Long) {
draftDao.loadDraftsSingle(accountId)
.flatMapObservable { Observable.fromIterable(it) }
.flatMapCompletable { draft ->
deleteDraftAndAttachments(draft)
}.subscribeOn(Schedulers.io())
.subscribe()
}
fun deleteAttachments(draft: DraftEntity): Completable {
return Completable.fromCallable {
draft.attachments.forEach { attachment ->
if (context.contentResolver.delete(attachment.uri, null, null) == 0) {
Log.e("DraftHelper", "Did not delete file ${attachment.uriString}")
}
}
}.subscribeOn(Schedulers.io())
}
private fun Uri.isNotInFolder(folder: File): Boolean {
val filePath = path ?: return true
return File(filePath).parentFile == folder
}
private fun Uri.copyToFolder(folder: File): Uri {
val contentResolver = context.contentResolver
val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val mimeType = contentResolver.getType(this)
val map = MimeTypeMap.getSingleton()
val fileExtension = map.getExtensionFromMimeType(mimeType)
val filename = String.format("Tusky_Draft_Media_%s.%s", timeStamp, fileExtension)
val file = File(folder, filename)
IOUtils.copyToFile(contentResolver, this, file)
return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file)
}
}

View file

@ -0,0 +1,81 @@
/* Copyright 2020 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.components.drafts
import android.view.ViewGroup
import android.widget.ImageView
import androidx.appcompat.widget.AppCompatImageView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.db.DraftAttachment
class DraftMediaAdapter(
private val attachmentClick: () -> Unit
) : ListAdapter<DraftAttachment, DraftMediaAdapter.DraftMediaViewHolder>(
object: DiffUtil.ItemCallback<DraftAttachment>() {
override fun areItemsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean {
return oldItem == newItem
}
}
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DraftMediaViewHolder {
return DraftMediaViewHolder(AppCompatImageView(parent.context))
}
override fun onBindViewHolder(holder: DraftMediaViewHolder, position: Int) {
getItem(position)?.let { attachment ->
if (attachment.type == DraftAttachment.Type.AUDIO) {
holder.imageView.setImageResource(R.drawable.ic_music_box_preview_24dp)
} else {
Glide.with(holder.itemView.context)
.load(attachment.uri)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.dontAnimate()
.into(holder.imageView)
}
}
}
inner class DraftMediaViewHolder(val imageView: ImageView)
: RecyclerView.ViewHolder(imageView) {
init {
val thumbnailViewSize =
imageView.context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size)
val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize)
val margin = itemView.context.resources
.getDimensionPixelSize(R.dimen.compose_media_preview_margin)
val marginBottom = itemView.context.resources
.getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom)
layoutParams.setMargins(margin, 0, margin, marginBottom)
imageView.layoutParams = layoutParams
imageView.scaleType = ImageView.ScaleType.CENTER_CROP
imageView.setOnClickListener {
attachmentClick()
}
}
}
}

View file

@ -0,0 +1,197 @@
/* Copyright 2020 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.components.drafts
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.LinearLayout
import android.widget.Toast
import androidx.activity.viewModels
import androidx.lifecycle.Lifecycle
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.SavedTootActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.databinding.ActivityDraftsBinding
import com.keylesspalace.tusky.db.DraftEntity
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.uber.autodispose.android.lifecycle.autoDispose
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import retrofit2.HttpException
import javax.inject.Inject
class DraftsActivity : BaseActivity(), DraftActionListener {
@Inject
lateinit var viewModelFactory: ViewModelFactory
private val viewModel: DraftsViewModel by viewModels { viewModelFactory }
private lateinit var binding: ActivityDraftsBinding
private lateinit var bottomSheet: BottomSheetBehavior<LinearLayout>
private var oldDraftsButton: MenuItem? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityDraftsBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.includedToolbar.toolbar)
supportActionBar?.apply {
title = getString(R.string.title_drafts)
setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true)
}
binding.draftsErrorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_saved_status)
val adapter = DraftsAdapter(this)
binding.draftsRecyclerView.adapter = adapter
binding.draftsRecyclerView.layoutManager = LinearLayoutManager(this)
binding.draftsRecyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
bottomSheet = BottomSheetBehavior.from(binding.bottomSheet.root)
viewModel.drafts.observe(this) { draftList ->
if (draftList.isEmpty()) {
binding.draftsRecyclerView.hide()
binding.draftsErrorMessageView.show()
} else {
binding.draftsRecyclerView.show()
binding.draftsErrorMessageView.hide()
adapter.submitList(draftList)
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.drafts, menu)
oldDraftsButton = menu.findItem(R.id.action_old_drafts)
viewModel.showOldDraftsButton()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
.subscribe { showOldDraftsButton ->
oldDraftsButton?.isVisible = showOldDraftsButton
}
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
return true
}
R.id.action_old_drafts -> {
val intent = Intent(this, SavedTootActivity::class.java)
startActivityWithSlideInAnimation(intent)
return true
}
}
return super.onOptionsItemSelected(item)
}
override fun onOpenDraft(draft: DraftEntity) {
if (draft.inReplyToId != null) {
bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
viewModel.getToot(draft.inReplyToId)
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(this)
.subscribe({ status ->
val composeOptions = ComposeActivity.ComposeOptions(
draftId = draft.id,
tootText = draft.content,
contentWarning = draft.contentWarning,
inReplyToId = draft.inReplyToId,
replyingStatusContent = status.content.toString(),
replyingStatusAuthor = status.account.localUsername,
draftAttachments = draft.attachments,
poll = draft.poll,
sensitive = draft.sensitive,
visibility = draft.visibility
)
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
startActivity(ComposeActivity.startIntent(this, composeOptions))
}, { throwable ->
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
Log.w(TAG, "failed loading reply information", throwable)
if (throwable is HttpException && throwable.code() == 404) {
// the original status to which a reply was drafted has been deleted
// let's open the ComposeActivity without reply information
Toast.makeText(this, getString(R.string.drafts_toot_reply_removed), Toast.LENGTH_LONG).show()
openDraftWithoutReply(draft)
} else {
Snackbar.make(binding.root, getString(R.string.drafts_failed_loading_reply), Snackbar.LENGTH_SHORT)
.show()
}
})
} else {
openDraftWithoutReply(draft)
}
}
private fun openDraftWithoutReply(draft: DraftEntity) {
val composeOptions = ComposeActivity.ComposeOptions(
draftId = draft.id,
tootText = draft.content,
contentWarning = draft.contentWarning,
draftAttachments = draft.attachments,
poll = draft.poll,
sensitive = draft.sensitive,
visibility = draft.visibility
)
startActivity(ComposeActivity.startIntent(this, composeOptions))
}
override fun onDeleteDraft(draft: DraftEntity) {
viewModel.deleteDraft(draft)
Snackbar.make(binding.root, getString(R.string.draft_deleted), Snackbar.LENGTH_LONG)
.setAction(R.string.action_undo) {
viewModel.restoreDraft(draft)
}
.show()
}
companion object {
const val TAG = "DraftsActivity"
fun newIntent(context: Context) = Intent(context, DraftsActivity::class.java)
}
}

View file

@ -0,0 +1,92 @@
/* Copyright 2021 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.components.drafts
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.databinding.ItemDraftBinding
import com.keylesspalace.tusky.db.DraftEntity
import com.keylesspalace.tusky.util.BindingViewHolder
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.visible
interface DraftActionListener {
fun onOpenDraft(draft: DraftEntity)
fun onDeleteDraft(draft: DraftEntity)
}
class DraftsAdapter(
private val listener: DraftActionListener
) : PagedListAdapter<DraftEntity, BindingViewHolder<ItemDraftBinding>>(
object : DiffUtil.ItemCallback<DraftEntity>() {
override fun areItemsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean {
return oldItem == newItem
}
}
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder<ItemDraftBinding> {
val binding = ItemDraftBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val viewHolder = BindingViewHolder(binding)
binding.draftMediaPreview.layoutManager = LinearLayoutManager(binding.root.context, RecyclerView.HORIZONTAL, false)
binding.draftMediaPreview.adapter = DraftMediaAdapter {
getItem(viewHolder.adapterPosition)?.let { draft ->
listener.onOpenDraft(draft)
}
}
return viewHolder
}
override fun onBindViewHolder(holder: BindingViewHolder<ItemDraftBinding>, position: Int) {
getItem(position)?.let { draft ->
holder.binding.root.setOnClickListener {
listener.onOpenDraft(draft)
}
holder.binding.deleteButton.setOnClickListener {
listener.onDeleteDraft(draft)
}
holder.binding.draftSendingInfo.visible(draft.failedToSend)
holder.binding.contentWarning.visible(!draft.contentWarning.isNullOrEmpty())
holder.binding.contentWarning.text = draft.contentWarning
holder.binding.content.text = draft.content
holder.binding.draftMediaPreview.visible(draft.attachments.isNotEmpty())
(holder.binding.draftMediaPreview.adapter as DraftMediaAdapter).submitList(draft.attachments)
if (draft.poll != null) {
holder.binding.draftPoll.show()
holder.binding.draftPoll.setPoll(draft.poll)
} else {
holder.binding.draftPoll.hide()
}
}
}
}

View file

@ -0,0 +1,69 @@
/* Copyright 2020 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.components.drafts
import androidx.lifecycle.ViewModel
import androidx.paging.toLiveData
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.db.DraftEntity
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi
import io.reactivex.Observable
import io.reactivex.Single
import javax.inject.Inject
class DraftsViewModel @Inject constructor(
val database: AppDatabase,
val accountManager: AccountManager,
val api: MastodonApi,
val draftHelper: DraftHelper
) : ViewModel() {
val drafts = database.draftDao().loadDrafts(accountManager.activeAccount?.id!!).toLiveData(pageSize = 20)
private val deletedDrafts: MutableList<DraftEntity> = mutableListOf()
fun showOldDraftsButton(): Observable<Boolean> {
return database.tootDao().savedTootCount()
.map { count -> count > 0 }
}
fun deleteDraft(draft: DraftEntity) {
// this does not immediately delete media files to avoid unnecessary file operations
// in case the user decides to restore the draft
database.draftDao().delete(draft.id)
.subscribe()
deletedDrafts.add(draft)
}
fun restoreDraft(draft: DraftEntity) {
database.draftDao().insertOrReplace(draft)
.subscribe()
deletedDrafts.remove(draft)
}
fun getToot(tootId: String): Single<Status> {
return api.status(tootId)
}
override fun onCleared() {
deletedDrafts.forEach {
draftHelper.deleteAttachments(it).subscribe()
}
}
}

View file

@ -1,7 +1,6 @@
package com.keylesspalace.tusky.components.instancemute package com.keylesspalace.tusky.components.instancemute
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem
import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment
@ -32,16 +31,6 @@ class InstanceListActivity: BaseActivity(), HasAndroidInjector {
.commit() .commit()
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
return true
}
}
return super.onOptionsItemSelected(item)
}
override fun androidInjector() = androidInjector override fun androidInjector() = androidInjector
} }

View file

@ -5,6 +5,7 @@ import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -14,7 +15,6 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.instancemute.adapter.DomainMutesAdapter import com.keylesspalace.tusky.components.instancemute.adapter.DomainMutesAdapter
import com.keylesspalace.tusky.components.instancemute.interfaces.InstanceActionListener import com.keylesspalace.tusky.components.instancemute.interfaces.InstanceActionListener
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.fragment.BaseFragment
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.HttpHeaderLink import com.keylesspalace.tusky.util.HttpHeaderLink
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
@ -30,7 +30,7 @@ import retrofit2.Response
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
class InstanceListFragment: BaseFragment(), Injectable, InstanceActionListener { class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectable, InstanceActionListener {
@Inject @Inject
lateinit var api: MastodonApi lateinit var api: MastodonApi
@ -39,10 +39,6 @@ class InstanceListFragment: BaseFragment(), Injectable, InstanceActionListener {
private var adapter = DomainMutesAdapter(this) private var adapter = DomainMutesAdapter(this)
private lateinit var scrollListener: EndlessOnScrollListener private lateinit var scrollListener: EndlessOnScrollListener
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_instance_list, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)

View file

@ -121,7 +121,7 @@ public class NotificationHelper {
public static final String CHANNEL_BOOST = "CHANNEL_BOOST"; public static final String CHANNEL_BOOST = "CHANNEL_BOOST";
public static final String CHANNEL_FAVOURITE = "CHANNEL_FAVOURITE"; public static final String CHANNEL_FAVOURITE = "CHANNEL_FAVOURITE";
public static final String CHANNEL_POLL = "CHANNEL_POLL"; public static final String CHANNEL_POLL = "CHANNEL_POLL";
public static final String CHANNEL_SUBSCRIPTIONS = "CHANNEL_SUBSCRIPTIONS";
/** /**
* WorkManager Tag * WorkManager Tag
@ -138,6 +138,7 @@ public class NotificationHelper {
*/ */
public static void make(final Context context, Notification body, AccountEntity account, boolean isFirstOfBatch) { public static void make(final Context context, Notification body, AccountEntity account, boolean isFirstOfBatch) {
body = body.rewriteToStatusTypeIfNeeded(account.getAccountId());
if (!filterNotification(account, body, context)) { if (!filterNotification(account, body, context)) {
return; return;
@ -355,6 +356,7 @@ public class NotificationHelper {
CHANNEL_BOOST + account.getIdentifier(), CHANNEL_BOOST + account.getIdentifier(),
CHANNEL_FAVOURITE + account.getIdentifier(), CHANNEL_FAVOURITE + account.getIdentifier(),
CHANNEL_POLL + account.getIdentifier(), CHANNEL_POLL + account.getIdentifier(),
CHANNEL_SUBSCRIPTIONS + account.getIdentifier(),
}; };
int[] channelNames = { int[] channelNames = {
R.string.notification_mention_name, R.string.notification_mention_name,
@ -362,7 +364,8 @@ public class NotificationHelper {
R.string.notification_follow_request_name, R.string.notification_follow_request_name,
R.string.notification_boost_name, R.string.notification_boost_name,
R.string.notification_favourite_name, R.string.notification_favourite_name,
R.string.notification_poll_name R.string.notification_poll_name,
R.string.notification_subscription_name,
}; };
int[] channelDescriptions = { int[] channelDescriptions = {
R.string.notification_mention_descriptions, R.string.notification_mention_descriptions,
@ -370,7 +373,8 @@ public class NotificationHelper {
R.string.notification_follow_request_description, R.string.notification_follow_request_description,
R.string.notification_boost_description, R.string.notification_boost_description,
R.string.notification_favourite_description, R.string.notification_favourite_description,
R.string.notification_poll_description R.string.notification_poll_description,
R.string.notification_subscription_description,
}; };
List<NotificationChannel> channels = new ArrayList<>(6); List<NotificationChannel> channels = new ArrayList<>(6);
@ -516,6 +520,8 @@ public class NotificationHelper {
switch (notification.getType()) { switch (notification.getType()) {
case MENTION: case MENTION:
return account.getNotificationsMentioned(); return account.getNotificationsMentioned();
case STATUS:
return account.getNotificationsSubscriptions();
case FOLLOW: case FOLLOW:
return account.getNotificationsFollowed(); return account.getNotificationsFollowed();
case FOLLOW_REQUEST: case FOLLOW_REQUEST:
@ -536,6 +542,8 @@ public class NotificationHelper {
switch (notification.getType()) { switch (notification.getType()) {
case MENTION: case MENTION:
return CHANNEL_MENTION + account.getIdentifier(); return CHANNEL_MENTION + account.getIdentifier();
case STATUS:
return CHANNEL_SUBSCRIPTIONS + account.getIdentifier();
case FOLLOW: case FOLLOW:
return CHANNEL_FOLLOW + account.getIdentifier(); return CHANNEL_FOLLOW + account.getIdentifier();
case FOLLOW_REQUEST: case FOLLOW_REQUEST:
@ -606,6 +614,9 @@ public class NotificationHelper {
case MENTION: case MENTION:
return String.format(context.getString(R.string.notification_mention_format), return String.format(context.getString(R.string.notification_mention_format),
accountName); accountName);
case STATUS:
return String.format(context.getString(R.string.notification_subscription_format),
accountName);
case FOLLOW: case FOLLOW:
return String.format(context.getString(R.string.notification_follow_format), return String.format(context.getString(R.string.notification_follow_format),
accountName); accountName);
@ -636,6 +647,7 @@ public class NotificationHelper {
case MENTION: case MENTION:
case FAVOURITE: case FAVOURITE:
case REBLOG: case REBLOG:
case STATUS:
if (!TextUtils.isEmpty(notification.getStatus().getSpoilerText())) { if (!TextUtils.isEmpty(notification.getStatus().getSpoilerText())) {
return notification.getStatus().getSpoilerText(); return notification.getStatus().getSpoilerText();
} else { } else {

View file

@ -111,6 +111,17 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Injectable {
true true
} }
} }
switchPreference {
setTitle(R.string.pref_title_notification_filter_subscriptions)
key = PrefKeys.NOTIFICATION_FILTER_SUBSCRIPTIONS
isIconSpaceReserved = false
isChecked = activeAccount.notificationsSubscriptions
setOnPreferenceChangeListener { _, newValue ->
updateAccount { it.notificationsSubscriptions = newValue as Boolean }
true
}
}
} }
preferenceCategory(R.string.pref_title_notification_alerts) { category -> preferenceCategory(R.string.pref_title_notification_alerts) { category ->

View file

@ -20,8 +20,8 @@ import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.MenuItem
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.MainActivity import com.keylesspalace.tusky.MainActivity
@ -59,33 +59,36 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
setDisplayShowHomeEnabled(true) setDisplayShowHomeEnabled(true)
} }
val fragment: Fragment = when (intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)) { val fragmentTag = "preference_fragment_$EXTRA_PREFERENCE_TYPE"
GENERAL_PREFERENCES -> {
setTitle(R.string.action_view_preferences)
PreferencesFragment.newInstance()
}
ACCOUNT_PREFERENCES -> {
setTitle(R.string.action_view_account_preferences)
AccountPreferencesFragment.newInstance()
}
NOTIFICATION_PREFERENCES -> {
setTitle(R.string.pref_title_edit_notification_settings)
NotificationPreferencesFragment.newInstance()
}
TAB_FILTER_PREFERENCES -> {
setTitle(R.string.pref_title_status_tabs)
TabFilterPreferencesFragment.newInstance()
}
PROXY_PREFERENCES -> {
setTitle(R.string.pref_title_http_proxy_settings)
ProxyPreferencesFragment.newInstance()
}
else -> throw IllegalArgumentException("preferenceType not known")
}
supportFragmentManager.beginTransaction() val fragment: Fragment = supportFragmentManager.findFragmentByTag(fragmentTag)
.replace(R.id.fragment_container, fragment) ?: when (intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)) {
.commit() GENERAL_PREFERENCES -> {
setTitle(R.string.action_view_preferences)
PreferencesFragment.newInstance()
}
ACCOUNT_PREFERENCES -> {
setTitle(R.string.action_view_account_preferences)
AccountPreferencesFragment.newInstance()
}
NOTIFICATION_PREFERENCES -> {
setTitle(R.string.pref_title_edit_notification_settings)
NotificationPreferencesFragment.newInstance()
}
TAB_FILTER_PREFERENCES -> {
setTitle(R.string.pref_title_status_tabs)
TabFilterPreferencesFragment.newInstance()
}
PROXY_PREFERENCES -> {
setTitle(R.string.pref_title_http_proxy_settings)
ProxyPreferencesFragment.newInstance()
}
else -> throw IllegalArgumentException("preferenceType not known")
}
supportFragmentManager.commit {
replace(R.id.fragment_container, fragment, fragmentTag)
}
restartActivitiesOnExit = intent.getBooleanExtra("restart", false) restartActivitiesOnExit = intent.getBooleanExtra("restart", false)
@ -101,16 +104,6 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this) PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this)
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun saveInstanceState(outState: Bundle) { private fun saveInstanceState(outState: Bundle) {
outState.putBoolean("restart", restartActivitiesOnExit) outState.putBoolean("restart", restartActivitiesOnExit)
} }

View file

@ -19,10 +19,14 @@ import android.os.Bundle
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.settings.* import com.keylesspalace.tusky.settings.*
import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.deserialize
import com.keylesspalace.tusky.util.getNonNullString import com.keylesspalace.tusky.util.getNonNullString
import com.keylesspalace.tusky.util.serialize
import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.colorInt
@ -35,6 +39,9 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
@Inject @Inject
lateinit var okhttpclient: OkHttpClient lateinit var okhttpclient: OkHttpClient
@Inject
lateinit var accountManager: AccountManager
private val iconSize by lazy { resources.getDimensionPixelSize(R.dimen.preference_icon_size) } private val iconSize by lazy { resources.getDimensionPixelSize(R.dimen.preference_icon_size) }
private var httpProxyPref: Preference? = null private var httpProxyPref: Preference? = null
@ -167,6 +174,13 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
setTitle(R.string.pref_title_enable_swipe_for_tabs) setTitle(R.string.pref_title_enable_swipe_for_tabs)
isSingleLineTitle = false isSingleLineTitle = false
} }
switchPreference {
setDefaultValue(false)
key = PrefKeys.ANIMATE_CUSTOM_EMOJIS
setTitle(R.string.pref_title_animate_custom_emojis)
isSingleLineTitle = false
}
} }
preferenceCategory(R.string.pref_title_browser_settings) { preferenceCategory(R.string.pref_title_browser_settings) {
@ -192,6 +206,45 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
} }
} }
preferenceCategory(R.string.pref_title_wellbeing_mode) {
switchPreference {
title = getString(R.string.limit_notifications)
setDefaultValue(false)
key = PrefKeys.WELLBEING_LIMITED_NOTIFICATIONS
setOnPreferenceChangeListener { _, value ->
for (account in accountManager.accounts) {
val notificationFilter = deserialize(account.notificationsFilter).toMutableSet()
if (value == true) {
notificationFilter.add(Notification.Type.FAVOURITE)
notificationFilter.add(Notification.Type.FOLLOW)
notificationFilter.add(Notification.Type.REBLOG)
} else {
notificationFilter.remove(Notification.Type.FAVOURITE)
notificationFilter.remove(Notification.Type.FOLLOW)
notificationFilter.remove(Notification.Type.REBLOG)
}
account.notificationsFilter = serialize(notificationFilter)
accountManager.saveAccount(account)
}
true
}
}
switchPreference {
title = getString(R.string.wellbeing_hide_stats_posts)
setDefaultValue(false)
key = PrefKeys.WELLBEING_HIDE_STATS_POSTS
}
switchPreference {
title = getString(R.string.wellbeing_hide_stats_profile)
setDefaultValue(false)
key = PrefKeys.WELLBEING_HIDE_STATS_PROFILE
}
}
preferenceCategory(R.string.pref_title_proxy_settings) { preferenceCategory(R.string.pref_title_proxy_settings) {
httpProxyPref = preference { httpProxyPref = preference {
setTitle(R.string.pref_title_http_proxy_settings) setTitle(R.string.pref_title_http_proxy_settings)

View file

@ -18,7 +18,6 @@ package com.keylesspalace.tusky.components.report
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem
import androidx.activity.viewModels import androidx.activity.viewModels
import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.BottomSheetActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
@ -30,7 +29,6 @@ import kotlinx.android.synthetic.main.activity_report.*
import kotlinx.android.synthetic.main.toolbar_basic.* import kotlinx.android.synthetic.main.toolbar_basic.*
import javax.inject.Inject import javax.inject.Inject
class ReportActivity : BottomSheetActivity(), HasAndroidInjector { class ReportActivity : BottomSheetActivity(), HasAndroidInjector {
@Inject @Inject
@ -120,16 +118,6 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector {
wizard.currentItem = 0 wizard.currentItem = 0
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
closeScreen()
return true
}
}
return super.onOptionsItemSelected(item)
}
companion object { companion object {
private const val ACCOUNT_ID = "account_id" private const val ACCOUNT_ID = "account_id"
private const val ACCOUNT_USERNAME = "account_username" private const val ACCOUNT_USERNAME = "account_username"

View file

@ -75,7 +75,7 @@ class StatusViewHolder(
sensitive, previewListener, viewState.isMediaShow(status.id, status.sensitive), sensitive, previewListener, viewState.isMediaShow(status.id, status.sensitive),
mediaViewHeight) mediaViewHeight)
statusViewHelper.setupPollReadonly(status.poll.toViewData(), status.emojis, statusDisplayOptions.useAbsoluteTime) statusViewHelper.setupPollReadonly(status.poll.toViewData(), status.emojis, statusDisplayOptions)
setCreatedAt(status.createdAt) setCreatedAt(status.createdAt)
} }
@ -89,7 +89,7 @@ class StatusViewHolder(
itemView.statusContentWarningButton.hide() itemView.statusContentWarningButton.hide()
itemView.statusContentWarningDescription.hide() itemView.statusContentWarningDescription.hide()
} else { } else {
val emojiSpoiler = status.spoilerText.emojify(status.emojis, itemView.statusContentWarningDescription) val emojiSpoiler = status.spoilerText.emojify(status.emojis, itemView.statusContentWarningDescription, statusDisplayOptions.animateEmojis)
itemView.statusContentWarningDescription.text = emojiSpoiler itemView.statusContentWarningDescription.text = emojiSpoiler
itemView.statusContentWarningDescription.show() itemView.statusContentWarningDescription.show()
itemView.statusContentWarningButton.show() itemView.statusContentWarningButton.show()
@ -122,7 +122,7 @@ class StatusViewHolder(
emojis: List<Emoji>, emojis: List<Emoji>,
listener: LinkListener) { listener: LinkListener) {
if (expanded) { if (expanded) {
val emojifiedText = content.emojify(emojis, itemView.statusContent) val emojifiedText = content.emojify(emojis, itemView.statusContent, statusDisplayOptions.animateEmojis)
LinkHelper.setClickableText(itemView.statusContent, emojifiedText, mentions, listener) LinkHelper.setClickableText(itemView.statusContent, emojifiedText, mentions, listener)
} else { } else {
LinkHelper.setClickableMentions(itemView.statusContent, mentions, listener) LinkHelper.setClickableMentions(itemView.statusContent, mentions, listener)

View file

@ -15,13 +15,10 @@
package com.keylesspalace.tusky.components.report.fragments package com.keylesspalace.tusky.components.report.fragments
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.activityViewModels
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.report.ReportViewModel
import com.keylesspalace.tusky.components.report.Screen import com.keylesspalace.tusky.components.report.Screen
@ -33,19 +30,12 @@ import com.keylesspalace.tusky.util.show
import kotlinx.android.synthetic.main.fragment_report_done.* import kotlinx.android.synthetic.main.fragment_report_done.*
import javax.inject.Inject import javax.inject.Inject
class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable {
class ReportDoneFragment : Fragment(), Injectable {
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
private val viewModel: ReportViewModel by viewModels({ requireActivity() }) { viewModelFactory } private val viewModel: ReportViewModel by activityViewModels { viewModelFactory }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_report_done, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
textReported.text = getString(R.string.report_sent_success, viewModel.accountUserName) textReported.text = getString(R.string.report_sent_success, viewModel.accountUserName)

View file

@ -16,12 +16,10 @@
package com.keylesspalace.tusky.components.report.fragments package com.keylesspalace.tusky.components.report.fragments
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.activityViewModels
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.report.ReportViewModel
@ -33,18 +31,12 @@ import kotlinx.android.synthetic.main.fragment_report_note.*
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
class ReportNoteFragment : Fragment(), Injectable { class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable {
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
private val viewModel: ReportViewModel by viewModels({ requireActivity() }) { viewModelFactory } private val viewModel: ReportViewModel by activityViewModels { viewModelFactory }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_report_note, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
fillViews() fillViews()

View file

@ -16,13 +16,11 @@
package com.keylesspalace.tusky.components.report.fragments package com.keylesspalace.tusky.components.report.fragments
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.activityViewModels
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -41,6 +39,7 @@ import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.CardViewMode import com.keylesspalace.tusky.util.CardViewMode
import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
@ -49,7 +48,7 @@ import com.keylesspalace.tusky.viewdata.AttachmentViewData
import kotlinx.android.synthetic.main.fragment_report_statuses.* import kotlinx.android.synthetic.main.fragment_report_statuses.*
import javax.inject.Inject import javax.inject.Inject
class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler { class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Injectable, AdapterHandler {
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
@ -57,10 +56,9 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler {
@Inject @Inject
lateinit var accountManager: AccountManager lateinit var accountManager: AccountManager
private val viewModel: ReportViewModel by viewModels({ requireActivity() }) { viewModelFactory } private val viewModel: ReportViewModel by activityViewModels { viewModelFactory }
private lateinit var adapter: StatusesAdapter private lateinit var adapter: StatusesAdapter
private lateinit var layoutManager: LinearLayoutManager
private var snackbarErrorRetry: Snackbar? = null private var snackbarErrorRetry: Snackbar? = null
@ -88,12 +86,6 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler {
} }
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_report_statuses, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
handleClicks() handleClicks()
initStatusesView() initStatusesView()
@ -118,15 +110,16 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler {
showBotOverlay = false, showBotOverlay = false,
useBlurhash = preferences.getBoolean("useBlurhash", true), useBlurhash = preferences.getBoolean("useBlurhash", true),
cardViewMode = CardViewMode.NONE, cardViewMode = CardViewMode.NONE,
confirmReblogs = preferences.getBoolean("confirmReblogs", true) confirmReblogs = preferences.getBoolean("confirmReblogs", true),
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
) )
adapter = StatusesAdapter(statusDisplayOptions, adapter = StatusesAdapter(statusDisplayOptions,
viewModel.statusViewState, this) viewModel.statusViewState, this)
recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)) recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
layoutManager = LinearLayoutManager(requireContext()) recyclerView.layoutManager = LinearLayoutManager(requireContext())
recyclerView.layoutManager = layoutManager
recyclerView.adapter = adapter recyclerView.adapter = adapter
(recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false (recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false

View file

@ -18,7 +18,6 @@ package com.keylesspalace.tusky.components.scheduled
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -104,23 +103,13 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
} }
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun refreshStatuses() { private fun refreshStatuses() {
viewModel.reload() viewModel.reload()
} }
override fun edit(item: ScheduledStatus) { override fun edit(item: ScheduledStatus) {
val intent = ComposeActivity.startIntent(this, ComposeActivity.ComposeOptions( val intent = ComposeActivity.startIntent(this, ComposeActivity.ComposeOptions(
scheduledTootUid = item.id, scheduledTootId = item.id,
tootText = item.params.text, tootText = item.params.text,
contentWarning = item.params.spoilerText, contentWarning = item.params.spoilerText,
mediaAttachments = item.mediaAttachments, mediaAttachments = item.mediaAttachments,

View file

@ -20,7 +20,6 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
@ -82,17 +81,7 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector {
return true return true
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { private fun getPageTitle(position: Int): CharSequence {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
return true
}
}
return super.onOptionsItemSelected(item)
}
private fun getPageTitle(position: Int): CharSequence? {
return when (position) { return when (position) {
0 -> getString(R.string.title_statuses) 0 -> getString(R.string.title_statuses)
1 -> getString(R.string.title_accounts) 1 -> getString(R.string.title_accounts)

View file

@ -193,8 +193,8 @@ class SearchViewModel @Inject constructor(
return accountManager.getAllAccountsOrderedByActive() return accountManager.getAllAccountsOrderedByActive()
} }
fun muteAccount(accountId: String, notifications: Boolean) { fun muteAccount(accountId: String, notifications: Boolean, duration: Int) {
timelineCases.mute(accountId, notifications) timelineCases.mute(accountId, notifications, duration)
} }
fun pinAccount(status: Status, isPin: Boolean) { fun pinAccount(status: Status, isPin: Boolean) {

View file

@ -25,7 +25,7 @@ import com.keylesspalace.tusky.adapter.AccountViewHolder
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.interfaces.LinkListener
class SearchAccountsAdapter(private val linkListener: LinkListener) class SearchAccountsAdapter(private val linkListener: LinkListener, private val animateAvatars: Boolean, private val animateEmojis: Boolean)
: PagedListAdapter<Account, RecyclerView.ViewHolder>(ACCOUNT_COMPARATOR) { : PagedListAdapter<Account, RecyclerView.ViewHolder>(ACCOUNT_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
@ -37,7 +37,7 @@ class SearchAccountsAdapter(private val linkListener: LinkListener)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
getItem(position)?.let { item -> getItem(position)?.let { item ->
(holder as AccountViewHolder).apply { (holder as AccountViewHolder).apply {
setupWithAccount(item) setupWithAccount(item, animateAvatars, animateEmojis)
setupLinkListener(linkListener) setupLinkListener(linkListener)
} }
} }

View file

@ -18,12 +18,23 @@ package com.keylesspalace.tusky.components.search.fragments
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.paging.PagedList import androidx.paging.PagedList
import androidx.paging.PagedListAdapter import androidx.paging.PagedListAdapter
import androidx.preference.PreferenceManager
import com.keylesspalace.tusky.components.search.adapter.SearchAccountsAdapter import com.keylesspalace.tusky.components.search.adapter.SearchAccountsAdapter
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.NetworkState
import kotlinx.android.synthetic.main.fragment_search.*
class SearchAccountsFragment : SearchFragment<Account>() { class SearchAccountsFragment : SearchFragment<Account>() {
override fun createAdapter(): PagedListAdapter<Account, *> = SearchAccountsAdapter(this) override fun createAdapter(): PagedListAdapter<Account, *> {
val preferences = PreferenceManager.getDefaultSharedPreferences(searchRecyclerView.context)
return SearchAccountsAdapter(
this,
preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
)
}
override val networkStateRefresh: LiveData<NetworkState> override val networkStateRefresh: LiveData<NetworkState>
get() = viewModel.networkStateAccountRefresh get() = viewModel.networkStateAccountRefresh

View file

@ -1,11 +1,9 @@
package com.keylesspalace.tusky.components.search.fragments package com.keylesspalace.tusky.components.search.fragments
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.paging.PagedList import androidx.paging.PagedList
import androidx.paging.PagedListAdapter import androidx.paging.PagedListAdapter
@ -26,13 +24,13 @@ import com.keylesspalace.tusky.util.*
import kotlinx.android.synthetic.main.fragment_search.* import kotlinx.android.synthetic.main.fragment_search.*
import javax.inject.Inject import javax.inject.Inject
abstract class SearchFragment<T> : Fragment(), abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
LinkListener, Injectable, SwipeRefreshLayout.OnRefreshListener { LinkListener, Injectable, SwipeRefreshLayout.OnRefreshListener {
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
protected val viewModel: SearchViewModel by viewModels({ requireActivity() }) { viewModelFactory } protected val viewModel: SearchViewModel by activityViewModels { viewModelFactory }
private var snackbarErrorRetry: Snackbar? = null private var snackbarErrorRetry: Snackbar? = null
@ -43,12 +41,7 @@ abstract class SearchFragment<T> : Fragment(),
abstract val data: LiveData<PagedList<T>> abstract val data: LiveData<PagedList<T>>
protected lateinit var adapter: PagedListAdapter<T, *> protected lateinit var adapter: PagedListAdapter<T, *>
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_search, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initAdapter() initAdapter()
setupSwipeRefreshLayout() setupSwipeRefreshLayout()
subscribeObservables() subscribeObservables()

View file

@ -52,7 +52,9 @@ import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.entity.Status.Mention import com.keylesspalace.tusky.entity.Status.Mention
import com.keylesspalace.tusky.interfaces.AccountSelectionListener import com.keylesspalace.tusky.interfaces.AccountSelectionListener
import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.CardViewMode import com.keylesspalace.tusky.util.CardViewMode
import com.keylesspalace.tusky.util.LinkHelper
import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.NetworkState
import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.view.showMuteAccountDialog import com.keylesspalace.tusky.view.showMuteAccountDialog
@ -84,7 +86,9 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
showBotOverlay = preferences.getBoolean("showBotOverlay", true), showBotOverlay = preferences.getBoolean("showBotOverlay", true),
useBlurhash = preferences.getBoolean("useBlurhash", true), useBlurhash = preferences.getBoolean("useBlurhash", true),
cardViewMode = CardViewMode.NONE, cardViewMode = CardViewMode.NONE,
confirmReblogs = preferences.getBoolean("confirmReblogs", true) confirmReblogs = preferences.getBoolean("confirmReblogs", true),
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
) )
searchRecyclerView.addItemDecoration(DividerItemDecoration(searchRecyclerView.context, DividerItemDecoration.VERTICAL)) searchRecyclerView.addItemDecoration(DividerItemDecoration(searchRecyclerView.context, DividerItemDecoration.VERTICAL))
@ -141,6 +145,7 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
} }
} }
Attachment.Type.UNKNOWN -> { Attachment.Type.UNKNOWN -> {
LinkHelper.openLink(actionable.attachments[attachmentIndex].url, context)
} }
} }
@ -375,8 +380,8 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
showMuteAccountDialog( showMuteAccountDialog(
this.requireActivity(), this.requireActivity(),
accountUsername accountUsername
) { notifications -> ) { notifications, duration ->
viewModel.muteAccount(accountId, notifications) viewModel.muteAccount(accountId, notifications, duration)
} }
} }

View file

@ -43,6 +43,7 @@ data class AccountEntity(@field:PrimaryKey(autoGenerate = true) var id: Long,
var notificationsReblogged: Boolean = true, var notificationsReblogged: Boolean = true,
var notificationsFavorited: Boolean = true, var notificationsFavorited: Boolean = true,
var notificationsPolls: Boolean = true, var notificationsPolls: Boolean = true,
var notificationsSubscriptions: Boolean = true,
var notificationSound: Boolean = true, var notificationSound: Boolean = true,
var notificationVibration: Boolean = true, var notificationVibration: Boolean = true,
var notificationLight: Boolean = true, var notificationLight: Boolean = true,

View file

@ -35,7 +35,8 @@ class AccountManager @Inject constructor(db: AppDatabase) {
@Volatile @Volatile
var activeAccount: AccountEntity? = null var activeAccount: AccountEntity? = null
private var accounts: MutableList<AccountEntity> = mutableListOf() var accounts: MutableList<AccountEntity> = mutableListOf()
private set
private val accountDao: AccountDao = db.accountDao() private val accountDao: AccountDao = db.accountDao()
init { init {

View file

@ -15,22 +15,22 @@
package com.keylesspalace.tusky.db; package com.keylesspalace.tusky.db;
import com.keylesspalace.tusky.TabDataKt; import androidx.annotation.NonNull;
import com.keylesspalace.tusky.components.conversation.ConversationEntity;
import androidx.sqlite.db.SupportSQLiteDatabase;
import androidx.room.Database; import androidx.room.Database;
import androidx.room.RoomDatabase; import androidx.room.RoomDatabase;
import androidx.room.migration.Migration; import androidx.room.migration.Migration;
import androidx.annotation.NonNull; import androidx.sqlite.db.SupportSQLiteDatabase;
import com.keylesspalace.tusky.TabDataKt;
import com.keylesspalace.tusky.components.conversation.ConversationEntity;
/** /**
* DB version & declare DAO * DB version & declare DAO
*/ */
@Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, @Database(entities = { TootEntity.class, DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
TimelineAccountEntity.class, ConversationEntity.class TimelineAccountEntity.class, ConversationEntity.class
}, version = 23) }, version = 25)
public abstract class AppDatabase extends RoomDatabase { public abstract class AppDatabase extends RoomDatabase {
public abstract TootDao tootDao(); public abstract TootDao tootDao();
@ -38,6 +38,7 @@ public abstract class AppDatabase extends RoomDatabase {
public abstract InstanceDao instanceDao(); public abstract InstanceDao instanceDao();
public abstract ConversationsDao conversationDao(); public abstract ConversationsDao conversationDao();
public abstract TimelineDao timelineDao(); public abstract TimelineDao timelineDao();
public abstract DraftDao draftDao();
public static final Migration MIGRATION_2_3 = new Migration(2, 3) { public static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override @Override
@ -46,7 +47,6 @@ public abstract class AppDatabase extends RoomDatabase {
database.execSQL("INSERT INTO TootEntity2 SELECT * FROM TootEntity;"); database.execSQL("INSERT INTO TootEntity2 SELECT * FROM TootEntity;");
database.execSQL("DROP TABLE TootEntity;"); database.execSQL("DROP TABLE TootEntity;");
database.execSQL("ALTER TABLE TootEntity2 RENAME TO TootEntity;"); database.execSQL("ALTER TABLE TootEntity2 RENAME TO TootEntity;");
} }
}; };
@ -339,5 +339,30 @@ public abstract class AppDatabase extends RoomDatabase {
database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `muted` INTEGER"); database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `muted` INTEGER");
} }
}; };
public static final Migration MIGRATION_23_24 = new Migration(23, 24) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsSubscriptions` INTEGER NOT NULL DEFAULT 1");
}
};
public static final Migration MIGRATION_24_25 = new Migration(24, 25) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL(
"CREATE TABLE IF NOT EXISTS `DraftEntity` (" +
"`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " +
"`accountId` INTEGER NOT NULL, " +
"`inReplyToId` TEXT," +
"`content` TEXT," +
"`contentWarning` TEXT," +
"`sensitive` INTEGER NOT NULL," +
"`visibility` INTEGER NOT NULL," +
"`attachments` TEXT NOT NULL," +
"`poll` TEXT," +
"`failedToSend` INTEGER NOT NULL)"
);
}
};
} }

View file

@ -24,10 +24,7 @@ import com.google.gson.reflect.TypeToken
import com.keylesspalace.tusky.TabData import com.keylesspalace.tusky.TabData
import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity
import com.keylesspalace.tusky.createTabDataFromId import com.keylesspalace.tusky.createTabDataFromId
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.*
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Poll
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.json.SpannedTypeAdapter import com.keylesspalace.tusky.json.SpannedTypeAdapter
import com.keylesspalace.tusky.util.trimTrailingWhitespace import com.keylesspalace.tusky.util.trimTrailingWhitespace
import java.net.URLDecoder import java.net.URLDecoder
@ -151,4 +148,23 @@ class Converters {
return gson.fromJson(pollJson, Poll::class.java) return gson.fromJson(pollJson, Poll::class.java)
} }
} @TypeConverter
fun newPollToJson(newPoll: NewPoll?): String? {
return gson.toJson(newPoll)
}
@TypeConverter
fun jsonToNewPoll(newPollJson: String?): NewPoll? {
return gson.fromJson(newPollJson, NewPoll::class.java)
}
@TypeConverter
fun draftAttachmentListToJson(draftAttachments: List<DraftAttachment>?): String? {
return gson.toJson(draftAttachments)
}
@TypeConverter
fun jsonToDraftAttachmentList(draftAttachmentListJson: String?): List<DraftAttachment>? {
return gson.fromJson(draftAttachmentListJson, object : TypeToken<List<DraftAttachment>>() {}.type)
}
}

View file

@ -0,0 +1,44 @@
/* Copyright 2020 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.db
import androidx.paging.DataSource
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import io.reactivex.Completable
import io.reactivex.Single
@Dao
interface DraftDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertOrReplace(draft: DraftEntity): Completable
@Query("SELECT * FROM DraftEntity WHERE accountId = :accountId ORDER BY id ASC")
fun loadDrafts(accountId: Long): DataSource.Factory<Int, DraftEntity>
@Query("SELECT * FROM DraftEntity WHERE accountId = :accountId")
fun loadDraftsSingle(accountId: Long): Single<List<DraftEntity>>
@Query("DELETE FROM DraftEntity WHERE id = :id")
fun delete(id: Int): Completable
@Query("SELECT * FROM DraftEntity WHERE id = :id")
fun find(id: Int): Single<DraftEntity?>
}

View file

@ -0,0 +1,55 @@
/* Copyright 2020 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.db
import android.net.Uri
import android.os.Parcelable
import androidx.core.net.toUri
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import com.keylesspalace.tusky.entity.NewPoll
import com.keylesspalace.tusky.entity.Status
import kotlinx.android.parcel.Parcelize
@Entity
@TypeConverters(Converters::class)
data class DraftEntity(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val accountId: Long,
val inReplyToId: String?,
val content: String?,
val contentWarning: String?,
val sensitive: Boolean,
val visibility: Status.Visibility,
val attachments: List<DraftAttachment>,
val poll: NewPoll?,
val failedToSend: Boolean
)
@Parcelize
data class DraftAttachment(
val uriString: String,
val description: String?,
val type: Type
): Parcelable {
val uri: Uri
get() = uriString.toUri()
enum class Type {
IMAGE, VIDEO, AUDIO;
}
}

View file

@ -26,7 +26,7 @@ import com.keylesspalace.tusky.entity.Status
// Avoiding rescanning status table when accounts table changes. Recommended by Room(c). // Avoiding rescanning status table when accounts table changes. Recommended by Room(c).
indices = [Index("authorServerId", "timelineUserId")] indices = [Index("authorServerId", "timelineUserId")]
) )
@TypeConverters(TootEntity.Converters::class) @TypeConverters(Converters::class)
data class TimelineStatusEntity( data class TimelineStatusEntity(
val serverId: String, // id never flips: we need it for sorting so it's a real id val serverId: String, // id never flips: we need it for sorting so it's a real id
val url: String?, val url: String?,

View file

@ -16,12 +16,12 @@
package com.keylesspalace.tusky.db; package com.keylesspalace.tusky.db;
import androidx.room.Dao; import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query; import androidx.room.Query;
import java.util.List; import java.util.List;
import io.reactivex.Observable;
/** /**
* Created by cto3543 on 28/06/2017. * Created by cto3543 on 28/06/2017.
* *
@ -30,8 +30,6 @@ import java.util.List;
@Dao @Dao
public interface TootDao { public interface TootDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertOrReplace(TootEntity users);
@Query("SELECT * FROM TootEntity ORDER BY uid DESC") @Query("SELECT * FROM TootEntity ORDER BY uid DESC")
List<TootEntity> loadAll(); List<TootEntity> loadAll();
@ -41,4 +39,7 @@ public interface TootDao {
@Query("SELECT * FROM TootEntity WHERE uid = :uid") @Query("SELECT * FROM TootEntity WHERE uid = :uid")
TootEntity find(int uid); TootEntity find(int uid);
}
@Query("SELECT COUNT(*) FROM TootEntity")
Observable<Integer> savedTootCount();
}

View file

@ -18,6 +18,7 @@ package com.keylesspalace.tusky.di
import com.keylesspalace.tusky.* import com.keylesspalace.tusky.*
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.drafts.DraftsActivity
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
import com.keylesspalace.tusky.components.preference.PreferencesActivity import com.keylesspalace.tusky.components.preference.PreferencesActivity
import com.keylesspalace.tusky.components.report.ReportActivity import com.keylesspalace.tusky.components.report.ReportActivity
@ -107,4 +108,7 @@ abstract class ActivitiesModule {
@ContributesAndroidInjector @ContributesAndroidInjector
abstract fun contributesAnnouncementsActivity(): AnnouncementsActivity abstract fun contributesAnnouncementsActivity(): AnnouncementsActivity
@ContributesAndroidInjector
abstract fun contributesDraftActivity(): DraftsActivity
} }

View file

@ -80,7 +80,7 @@ class AppModule {
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, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22, AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22,
AppDatabase.MIGRATION_22_23) AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24, AppDatabase.MIGRATION_24_25)
.build() .build()
} }
@ -88,4 +88,4 @@ class AppModule {
@Singleton @Singleton
fun notifier(context: Context): Notifier = SystemNotifier(context) fun notifier(context: Context): Notifier = SystemNotifier(context)
} }

View file

@ -16,6 +16,8 @@
package com.keylesspalace.tusky.di package com.keylesspalace.tusky.di
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import android.text.Spanned import android.text.Spanned
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
@ -24,15 +26,20 @@ import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.json.SpannedTypeAdapter import com.keylesspalace.tusky.json.SpannedTypeAdapter
import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.okhttpClient import com.keylesspalace.tusky.util.getNonNullString
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import okhttp3.Cache
import okhttp3.OkHttp
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.create import retrofit2.create
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.concurrent.TimeUnit
import javax.inject.Singleton import javax.inject.Singleton
/** /**
@ -54,9 +61,37 @@ class NetworkModule {
@Singleton @Singleton
fun providesHttpClient( fun providesHttpClient(
accountManager: AccountManager, accountManager: AccountManager,
context: Context context: Context,
preferences: SharedPreferences
): OkHttpClient { ): OkHttpClient {
return okhttpClient(context) val httpProxyEnabled = preferences.getBoolean("httpProxyEnabled", false)
val httpServer = preferences.getNonNullString("httpProxyServer", "")
val httpPort = preferences.getNonNullString("httpProxyPort", "-1").toIntOrNull() ?: -1
val cacheSize = 25 * 1024 * 1024L // 25 MiB
val builder = OkHttpClient.Builder()
.addInterceptor { chain ->
/**
* Add a custom User-Agent that contains Tusky, Android and OkHttp Version to all requests
* Example:
* User-Agent: Tusky/1.1.2 Android/5.0.2 OkHttp/4.9.0
* */
val requestWithUserAgent = chain.request().newBuilder()
.header(
"User-Agent",
"Tusky/${BuildConfig.VERSION_NAME} Android/${Build.VERSION.RELEASE} OkHttp/${OkHttp.VERSION}"
)
.build()
chain.proceed(requestWithUserAgent)
}
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.cache(Cache(context.cacheDir, cacheSize))
if (httpProxyEnabled && httpServer.isNotEmpty() && httpPort > 0 && httpPort < 65535) {
val address = InetSocketAddress.createUnresolved(httpServer, httpPort)
builder.proxy(Proxy(Proxy.Type.HTTP, address))
}
return builder
.apply { .apply {
addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) addInterceptor(InstanceSwitchAuthInterceptor(accountManager))
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {

View file

@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModelProvider
import com.keylesspalace.tusky.components.announcements.AnnouncementsViewModel import com.keylesspalace.tusky.components.announcements.AnnouncementsViewModel
import com.keylesspalace.tusky.components.compose.ComposeViewModel import com.keylesspalace.tusky.components.compose.ComposeViewModel
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
import com.keylesspalace.tusky.components.drafts.DraftsViewModel
import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.report.ReportViewModel
import com.keylesspalace.tusky.components.scheduled.ScheduledTootViewModel import com.keylesspalace.tusky.components.scheduled.ScheduledTootViewModel
import com.keylesspalace.tusky.components.search.SearchViewModel import com.keylesspalace.tusky.components.search.SearchViewModel
@ -91,5 +92,10 @@ abstract class ViewModelModule {
@ViewModelKey(AnnouncementsViewModel::class) @ViewModelKey(AnnouncementsViewModel::class)
internal abstract fun announcementsViewModel(viewModel: AnnouncementsViewModel): ViewModel internal abstract fun announcementsViewModel(viewModel: AnnouncementsViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(DraftsViewModel::class)
internal abstract fun draftsViewModel(viewModel: DraftsViewModel): ViewModel
//Add more ViewModels here //Add more ViewModels here
} }

View file

@ -15,7 +15,10 @@
package com.keylesspalace.tusky.entity package com.keylesspalace.tusky.entity
import com.google.gson.* import com.google.gson.JsonDeserializationContext
import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonParseException
import com.google.gson.annotations.JsonAdapter import com.google.gson.annotations.JsonAdapter
data class Notification( data class Notification(
@ -32,7 +35,8 @@ data class Notification(
FAVOURITE("favourite"), FAVOURITE("favourite"),
FOLLOW("follow"), FOLLOW("follow"),
FOLLOW_REQUEST("follow_request"), FOLLOW_REQUEST("follow_request"),
POLL("poll"); POLL("poll"),
STATUS("status");
companion object { companion object {
@ -44,7 +48,7 @@ data class Notification(
} }
return UNKNOWN return UNKNOWN
} }
val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL) val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL, STATUS)
} }
override fun toString(): String { override fun toString(): String {
@ -72,4 +76,14 @@ data class Notification(
} }
} }
// for Pleroma compatibility that uses Mention type
fun rewriteToStatusTypeIfNeeded(accountId: String) : Notification {
if (type == Type.MENTION && status != null) {
return if (status.mentions.any {
it.id == accountId
}) this else copy(type = Type.STATUS)
}
return this
}
} }

View file

@ -26,6 +26,8 @@ data class Relationship (
@SerializedName("muting_notifications") val mutingNotifications: Boolean, @SerializedName("muting_notifications") val mutingNotifications: Boolean,
val requested: Boolean, val requested: Boolean,
@SerializedName("showing_reblogs") val showingReblogs: Boolean, @SerializedName("showing_reblogs") val showingReblogs: Boolean,
val subscribing: Boolean? = null, // Pleroma extension
@SerializedName("domain_blocking") val blockingDomain: Boolean, @SerializedName("domain_blocking") val blockingDomain: Boolean,
val note: String? // nullable for backward compatibility / feature detection val note: String?, // nullable for backward compatibility / feature detection
val notifying: Boolean? // since 3.3.0rc
) )

View file

@ -17,10 +17,10 @@ package com.keylesspalace.tusky.fragment
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -36,6 +36,7 @@ import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Relationship import com.keylesspalace.tusky.entity.Relationship
import com.keylesspalace.tusky.interfaces.AccountActionListener import com.keylesspalace.tusky.interfaces.AccountActionListener
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.HttpHeaderLink import com.keylesspalace.tusky.util.HttpHeaderLink
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
@ -45,14 +46,12 @@ import com.uber.autodispose.autoDispose
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.fragment_account_list.* import kotlinx.android.synthetic.main.fragment_account_list.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import java.io.IOException import java.io.IOException
import java.util.HashMap import java.util.*
import javax.inject.Inject import javax.inject.Inject
class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountActionListener, Injectable {
@Inject @Inject
lateinit var api: MastodonApi lateinit var api: MastodonApi
@ -71,10 +70,6 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable {
id = arguments?.getString(ARG_ID) id = arguments?.getString(ARG_ID)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return inflater.inflate(R.layout.fragment_account_list, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -85,11 +80,15 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable {
recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
val pm = PreferenceManager.getDefaultSharedPreferences(view.context)
val animateAvatar = pm.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false)
val animateEmojis = pm.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
adapter = when (type) { adapter = when (type) {
Type.BLOCKS -> BlocksAdapter(this) Type.BLOCKS -> BlocksAdapter(this, animateAvatar, animateEmojis)
Type.MUTES -> MutesAdapter(this) Type.MUTES -> MutesAdapter(this, animateAvatar, animateEmojis)
Type.FOLLOW_REQUESTS -> FollowRequestsAdapter(this) Type.FOLLOW_REQUESTS -> FollowRequestsAdapter(this, animateAvatar, animateEmojis)
else -> FollowAdapter(this) else -> FollowAdapter(this, animateAvatar, animateEmojis)
} }
recyclerView.adapter = adapter recyclerView.adapter = adapter
@ -202,27 +201,23 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable {
override fun onRespondToFollowRequest(accept: Boolean, accountId: String, override fun onRespondToFollowRequest(accept: Boolean, accountId: String,
position: Int) { position: Int) {
val callback = object : Callback<Relationship> { if (accept) {
override fun onResponse(call: Call<Relationship>, response: Response<Relationship>) {
if (response.isSuccessful) {
onRespondToFollowRequestSuccess(position)
} else {
onRespondToFollowRequestFailure(accept, accountId)
}
}
override fun onFailure(call: Call<Relationship>, t: Throwable) {
onRespondToFollowRequestFailure(accept, accountId)
}
}
val call = if (accept) {
api.authorizeFollowRequest(accountId) api.authorizeFollowRequest(accountId)
} else { } else {
api.rejectFollowRequest(accountId) api.rejectFollowRequest(accountId)
} }.observeOn(AndroidSchedulers.mainThread())
callList.add(call) .autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
call.enqueue(callback) .subscribe({
onRespondToFollowRequestSuccess(position)
}, { throwable ->
val verb = if (accept) {
"accept"
} else {
"reject"
}
Log.e(TAG, "Failed to $verb account id $accountId.", throwable)
})
} }
private fun onRespondToFollowRequestSuccess(position: Int) { private fun onRespondToFollowRequestSuccess(position: Int) {
@ -230,15 +225,6 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable {
followRequestsAdapter.removeItem(position) followRequestsAdapter.removeItem(position)
} }
private fun onRespondToFollowRequestFailure(accept: Boolean, accountId: String) {
val verb = if (accept) {
"accept"
} else {
"reject"
}
Log.e(TAG, "Failed to $verb account id $accountId.")
}
private fun getFetchCallByListType(fromId: String?): Single<Response<List<Account>>> { private fun getFetchCallByListType(fromId: String?): Single<Response<List<Account>>> {
return when (type) { return when (type) {
Type.FOLLOWS -> { Type.FOLLOWS -> {

View file

@ -18,12 +18,13 @@ package com.keylesspalace.tusky.fragment
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
@ -34,14 +35,17 @@ import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.interfaces.RefreshableFragment import com.keylesspalace.tusky.interfaces.RefreshableFragment
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.LinkHelper
import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.view.SquareImageView import com.keylesspalace.tusky.view.SquareImageView
import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.AttachmentViewData
import com.uber.autodispose.android.lifecycle.autoDispose
import io.reactivex.SingleObserver
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import kotlinx.android.synthetic.main.fragment_timeline.* import kotlinx.android.synthetic.main.fragment_timeline.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import java.io.IOException import java.io.IOException
import java.util.* import java.util.*
@ -53,7 +57,7 @@ import javax.inject.Inject
* Fragment with multiple columns of media previews for the specified account. * Fragment with multiple columns of media previews for the specified account.
*/ */
class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFragment, Injectable {
companion object { companion object {
@JvmStatic @JvmStatic
fun newInstance(accountId: String, enableSwipeToRefresh:Boolean=true): AccountMediaFragment { fun newInstance(accountId: String, enableSwipeToRefresh:Boolean=true): AccountMediaFragment {
@ -77,14 +81,13 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
lateinit var api: MastodonApi lateinit var api: MastodonApi
private val adapter = MediaGridAdapter() private val adapter = MediaGridAdapter()
private var currentCall: Call<List<Status>>? = null
private val statuses = mutableListOf<Status>() private val statuses = mutableListOf<Status>()
private var fetchingStatus = FetchingStatus.NOT_FETCHING private var fetchingStatus = FetchingStatus.NOT_FETCHING
private lateinit var accountId: String private lateinit var accountId: String
private val callback = object : Callback<List<Status>> { private val callback = object : SingleObserver<Response<List<Status>>> {
override fun onFailure(call: Call<List<Status>>?, t: Throwable?) { override fun onError(t: Throwable) {
fetchingStatus = FetchingStatus.NOT_FETCHING fetchingStatus = FetchingStatus.NOT_FETCHING
if (isAdded) { if (isAdded) {
@ -106,7 +109,7 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
Log.d(TAG, "Failed to fetch account media", t) Log.d(TAG, "Failed to fetch account media", t)
} }
override fun onResponse(call: Call<List<Status>>, response: Response<List<Status>>) { override fun onSuccess(response: Response<List<Status>>) {
fetchingStatus = FetchingStatus.NOT_FETCHING fetchingStatus = FetchingStatus.NOT_FETCHING
if (isAdded) { if (isAdded) {
swipeRefreshLayout.isRefreshing = false swipeRefreshLayout.isRefreshing = false
@ -127,22 +130,23 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
if (statuses.isEmpty()) { if (statuses.isEmpty()) {
statusView.show() statusView.show()
statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty)
null)
} }
} }
} }
} }
override fun onSubscribe(d: Disposable) {}
} }
private val bottomCallback = object : Callback<List<Status>> { private val bottomCallback = object : SingleObserver<Response<List<Status>>> {
override fun onFailure(call: Call<List<Status>>?, t: Throwable?) { override fun onError(t: Throwable) {
fetchingStatus = FetchingStatus.NOT_FETCHING fetchingStatus = FetchingStatus.NOT_FETCHING
Log.d(TAG, "Failed to fetch account media", t) Log.d(TAG, "Failed to fetch account media", t)
} }
override fun onResponse(call: Call<List<Status>>, response: Response<List<Status>>) { override fun onSuccess(response: Response<List<Status>>) {
fetchingStatus = FetchingStatus.NOT_FETCHING fetchingStatus = FetchingStatus.NOT_FETCHING
val body = response.body() val body = response.body()
body?.let { fetched -> body?.let { fetched ->
@ -159,6 +163,7 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
} }
} }
override fun onSubscribe(d: Disposable) { }
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -166,10 +171,6 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
isSwipeToRefreshEnabled = arguments?.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH,true) == true isSwipeToRefreshEnabled = arguments?.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH,true) == true
accountId = arguments?.getString(ACCOUNT_ID_ARG)!! accountId = arguments?.getString(ACCOUNT_ID_ARG)!!
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_timeline, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -201,8 +202,10 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
statuses.lastOrNull()?.let { (id) -> statuses.lastOrNull()?.let { (id) ->
Log.d(TAG, "Requesting statuses with max_id: ${id}, (bottom)") Log.d(TAG, "Requesting statuses with max_id: ${id}, (bottom)")
fetchingStatus = FetchingStatus.FETCHING_BOTTOM fetchingStatus = FetchingStatus.FETCHING_BOTTOM
currentCall = api.accountStatuses(accountId, id, null, null, null, true, null) api.accountStatuses(accountId, id, null, null, null, true, null)
currentCall?.enqueue(bottomCallback) .observeOn(AndroidSchedulers.mainThread())
.autoDispose(this@AccountMediaFragment, Lifecycle.Event.ON_DESTROY)
.subscribe(bottomCallback)
} }
} }
} }
@ -215,14 +218,15 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
private fun refresh() { private fun refresh() {
statusView.hide() statusView.hide()
if (fetchingStatus != FetchingStatus.NOT_FETCHING) return if (fetchingStatus != FetchingStatus.NOT_FETCHING) return
currentCall = if (statuses.isEmpty()) { if (statuses.isEmpty()) {
fetchingStatus = FetchingStatus.INITIAL_FETCHING fetchingStatus = FetchingStatus.INITIAL_FETCHING
api.accountStatuses(accountId, null, null, null, null, true, null) api.accountStatuses(accountId, null, null, null, null, true, null)
} else { } else {
fetchingStatus = FetchingStatus.REFRESHING fetchingStatus = FetchingStatus.REFRESHING
api.accountStatuses(accountId, null, statuses[0].id, null, null, true, null) api.accountStatuses(accountId, null, statuses[0].id, null, null, true, null)
} }.observeOn(AndroidSchedulers.mainThread())
currentCall?.enqueue(callback) .autoDispose(this, Lifecycle.Event.ON_DESTROY)
.subscribe(callback)
if (!isSwipeToRefreshEnabled) if (!isSwipeToRefreshEnabled)
topProgressBar?.show() topProgressBar?.show()
@ -234,8 +238,10 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
} }
if (fetchingStatus == FetchingStatus.NOT_FETCHING && statuses.isEmpty()) { if (fetchingStatus == FetchingStatus.NOT_FETCHING && statuses.isEmpty()) {
fetchingStatus = FetchingStatus.INITIAL_FETCHING fetchingStatus = FetchingStatus.INITIAL_FETCHING
currentCall = api.accountStatuses(accountId, null, null, null, null, true, null) api.accountStatuses(accountId, null, null, null, null, true, null)
currentCall?.enqueue(callback) .observeOn(AndroidSchedulers.mainThread())
.autoDispose(this@AccountMediaFragment, Lifecycle.Event.ON_DESTROY)
.subscribe(callback)
} }
else if (needToRefresh) else if (needToRefresh)
refresh() refresh()
@ -260,10 +266,8 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
} }
} }
Attachment.Type.UNKNOWN -> { Attachment.Type.UNKNOWN -> {
}/* Intentionally do nothing. This case is here is to handle when new attachment LinkHelper.openLink(items[currentIndex].attachment.url, context)
* types are added to the API before code is added here to handle them. So, the }
* best fallback is to just show the preview and ignore requests to view them. */
} }
} }
@ -340,5 +344,4 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
needToRefresh = true needToRefresh = true
} }
} }

View file

@ -1,43 +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.fragment;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import java.util.ArrayList;
import java.util.List;
import retrofit2.Call;
public class BaseFragment extends Fragment {
protected List<Call> callList;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
callList = new ArrayList<>();
}
@Override
public void onDestroy() {
for (Call call : callList) {
call.cancel();
}
super.onDestroy();
}
}

View file

@ -71,6 +71,7 @@ import com.keylesspalace.tusky.interfaces.AccountActionListener;
import com.keylesspalace.tusky.interfaces.ActionButtonActivity; import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
import com.keylesspalace.tusky.interfaces.ReselectableFragment; import com.keylesspalace.tusky.interfaces.ReselectableFragment;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.settings.PrefKeys;
import com.keylesspalace.tusky.util.CardViewMode; import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.Either; import com.keylesspalace.tusky.util.Either;
import com.keylesspalace.tusky.util.HttpHeaderLink; import com.keylesspalace.tusky.util.HttpHeaderLink;
@ -101,13 +102,11 @@ import at.connyduck.sparkbutton.helpers.Utils;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.Single; import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import kotlin.Unit; import kotlin.Unit;
import kotlin.collections.CollectionsKt; import kotlin.collections.CollectionsKt;
import kotlin.jvm.functions.Function1; import kotlin.jvm.functions.Function1;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import static com.keylesspalace.tusky.util.StringUtils.isLessThan; import static com.keylesspalace.tusky.util.StringUtils.isLessThan;
import static com.uber.autodispose.AutoDispose.autoDisposable; import static com.uber.autodispose.AutoDispose.autoDisposable;
@ -124,8 +123,9 @@ public class NotificationsFragment extends SFragment implements
private static final int LOAD_AT_ONCE = 30; private static final int LOAD_AT_ONCE = 30;
private int maxPlaceholderId = 0; private int maxPlaceholderId = 0;
private final Set<Notification.Type> notificationFilter = new HashSet<>();
private Set<Notification.Type> notificationFilter = new HashSet<>(); private final CompositeDisposable disposables = new CompositeDisposable();
private enum FetchEnd { private enum FetchEnd {
TOP, TOP,
@ -179,7 +179,9 @@ public class NotificationsFragment extends SFragment implements
@Override @Override
public NotificationViewData apply(Either<Placeholder, Notification> input) { public NotificationViewData apply(Either<Placeholder, Notification> input) {
if (input.isRight()) { if (input.isRight()) {
Notification notification = input.asRight(); Notification notification = input.asRight()
.rewriteToStatusTypeIfNeeded(accountManager.getActiveAccount().getAccountId());
return ViewDataUtils.notificationToViewData( return ViewDataUtils.notificationToViewData(
notification, notification,
alwaysShowSensitiveMedia, alwaysShowSensitiveMedia,
@ -249,7 +251,9 @@ public class NotificationsFragment extends SFragment implements
preferences.getBoolean("showBotOverlay", true), preferences.getBoolean("showBotOverlay", true),
preferences.getBoolean("useBlurhash", true), preferences.getBoolean("useBlurhash", true),
CardViewMode.NONE, CardViewMode.NONE,
preferences.getBoolean("confirmReblogs", true) preferences.getBoolean("confirmReblogs", true),
preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
); );
adapter = new NotificationsAdapter(accountManager.getActiveAccount().getAccountId(), adapter = new NotificationsAdapter(accountManager.getActiveAccount().getAccountId(),
@ -681,32 +685,21 @@ public class NotificationsFragment extends SFragment implements
updateAdapter(); updateAdapter();
//Execute clear notifications request //Execute clear notifications request
Call<ResponseBody> call = mastodonApi.clearNotifications(); mastodonApi.clearNotifications()
call.enqueue(new Callback<ResponseBody>() { .observeOn(AndroidSchedulers.mainThread())
@Override .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
public void onResponse(@NonNull Call<ResponseBody> call, @NonNull Response<ResponseBody> response) { .subscribe(
if (isAdded()) { response -> {
if (!response.isSuccessful()) { // nothing to do
//Reload notifications on failure },
fullyRefreshWithProgressBar(true); throwable -> {
} //Reload notifications on failure
} fullyRefreshWithProgressBar(true);
} });
@Override
public void onFailure(@NonNull Call<ResponseBody> call, @NonNull Throwable t) {
//Reload notifications on failure
fullyRefreshWithProgressBar(true);
}
});
callList.add(call);
} }
private void resetNotificationsLoad() { private void resetNotificationsLoad() {
for (Call callItem : callList) { disposables.clear();
callItem.cancel();
}
callList.clear();
bottomLoading = false; bottomLoading = false;
topLoading = false; topLoading = false;
@ -770,6 +763,8 @@ public class NotificationsFragment extends SFragment implements
return getString(R.string.notification_follow_request_name); return getString(R.string.notification_follow_request_name);
case POLL: case POLL:
return getString(R.string.notification_poll_name); return getString(R.string.notification_poll_name);
case STATUS:
return getString(R.string.notification_subscription_name);
default: default:
return "Unknown"; return "Unknown";
} }
@ -797,6 +792,7 @@ public class NotificationsFragment extends SFragment implements
private void loadNotificationsFilter() { private void loadNotificationsFilter() {
AccountEntity account = accountManager.getActiveAccount(); AccountEntity account = accountManager.getActiveAccount();
if (account != null) { if (account != null) {
notificationFilter.clear();
notificationFilter.addAll(NotificationTypeConverterKt.deserialize( notificationFilter.addAll(NotificationTypeConverterKt.deserialize(
account.getNotificationsFilter())); account.getNotificationsFilter()));
} }
@ -833,8 +829,8 @@ public class NotificationsFragment extends SFragment implements
@Override @Override
public void onRespondToFollowRequest(boolean accept, String id, int position) { public void onRespondToFollowRequest(boolean accept, String id, int position) {
Single<Relationship> request = accept ? Single<Relationship> request = accept ?
mastodonApi.authorizeFollowRequestObservable(id) : mastodonApi.authorizeFollowRequest(id) :
mastodonApi.rejectFollowRequestObservable(id); mastodonApi.rejectFollowRequest(id);
request.observeOn(AndroidSchedulers.mainThread()) request.observeOn(AndroidSchedulers.mainThread())
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
.subscribe( .subscribe(
@ -952,27 +948,20 @@ public class NotificationsFragment extends SFragment implements
bottomLoading = true; bottomLoading = true;
} }
Call<List<Notification>> call = mastodonApi.notifications(fromId, uptoId, LOAD_AT_ONCE, showNotificationsFilter ? notificationFilter : null); Disposable notificationCall = mastodonApi.notifications(fromId, uptoId, LOAD_AT_ONCE, showNotificationsFilter ? notificationFilter : null)
.observeOn(AndroidSchedulers.mainThread())
call.enqueue(new Callback<List<Notification>>() { .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
@Override .subscribe(
public void onResponse(@NonNull Call<List<Notification>> call, response -> {
@NonNull Response<List<Notification>> response) { if (response.isSuccessful()) {
if (response.isSuccessful()) { String linkHeader = response.headers().get("Link");
String linkHeader = response.headers().get("Link"); onFetchNotificationsSuccess(response.body(), linkHeader, fetchEnd, pos);
onFetchNotificationsSuccess(response.body(), linkHeader, fetchEnd, pos); } else {
} else { onFetchNotificationsFailure(new Exception(response.message()), fetchEnd, pos);
onFetchNotificationsFailure(new Exception(response.message()), fetchEnd, pos); }
} },
} throwable -> onFetchNotificationsFailure(throwable, fetchEnd, pos));
disposables.add(notificationCall);
@Override
public void onFailure(@NonNull Call<List<Notification>> call, @NonNull Throwable t) {
if (!call.isCanceled())
onFetchNotificationsFailure((Exception) t, fetchEnd, pos);
}
});
callList.add(call);
} }
private void onFetchNotificationsSuccess(List<Notification> notifications, String linkHeader, private void onFetchNotificationsSuccess(List<Notification> notifications, String linkHeader,
@ -1031,7 +1020,7 @@ public class NotificationsFragment extends SFragment implements
progressBar.setVisibility(View.GONE); progressBar.setVisibility(View.GONE);
} }
private void onFetchNotificationsFailure(Exception exception, FetchEnd fetchEnd, int position) { private void onFetchNotificationsFailure(Throwable throwable, FetchEnd fetchEnd, int position) {
swipeRefreshLayout.setRefreshing(false); swipeRefreshLayout.setRefreshing(false);
if (fetchEnd == FetchEnd.MIDDLE && !notifications.get(position).isRight()) { if (fetchEnd == FetchEnd.MIDDLE && !notifications.get(position).isRight()) {
Placeholder placeholder = notifications.get(position).asLeft(); Placeholder placeholder = notifications.get(position).asLeft();
@ -1043,7 +1032,7 @@ public class NotificationsFragment extends SFragment implements
this.statusView.setVisibility(View.VISIBLE); this.statusView.setVisibility(View.VISIBLE);
swipeRefreshLayout.setEnabled(false); swipeRefreshLayout.setEnabled(false);
this.showingError = true; this.showingError = true;
if (exception instanceof IOException) { if (throwable instanceof IOException) {
this.statusView.setup(R.drawable.elephant_offline, R.string.error_network, __ -> { this.statusView.setup(R.drawable.elephant_offline, R.string.error_network, __ -> {
this.progressBar.setVisibility(View.VISIBLE); this.progressBar.setVisibility(View.VISIBLE);
this.onRefresh(); this.onRefresh();
@ -1058,7 +1047,7 @@ public class NotificationsFragment extends SFragment implements
} }
updateFilterVisibility(); updateFilterVisibility();
} }
Log.e(TAG, "Fetch failure: " + exception.getMessage()); Log.e(TAG, "Fetch failure: " + throwable.getMessage());
if (fetchEnd == FetchEnd.TOP) { if (fetchEnd == FetchEnd.TOP) {
topLoading = false; topLoading = false;
@ -1273,6 +1262,12 @@ public class NotificationsFragment extends SFragment implements
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
String rawAccountNotificationFilter = accountManager.getActiveAccount().getNotificationsFilter();
Set<Notification.Type> accountNotificationFilter = NotificationTypeConverterKt.deserialize(rawAccountNotificationFilter);
if (!notificationFilter.equals(accountNotificationFilter)) {
loadNotificationsFilter();
fullyRefreshWithProgressBar(true);
}
startUpdateTimestamp(); startUpdateTimestamp();
} }

View file

@ -20,7 +20,6 @@ import android.app.DownloadManager;
import android.content.ClipData; import android.content.ClipData;
import android.content.ClipboardManager; import android.content.ClipboardManager;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
@ -30,8 +29,6 @@ import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.CheckBox;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -41,14 +38,14 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.PopupMenu; import androidx.appcompat.widget.PopupMenu;
import androidx.core.app.ActivityOptionsCompat; import androidx.core.app.ActivityOptionsCompat;
import androidx.core.view.ViewCompat; import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Lifecycle; import androidx.lifecycle.Lifecycle;
import androidx.preference.PreferenceManager;
import com.keylesspalace.tusky.BaseActivity; import com.keylesspalace.tusky.BaseActivity;
import com.keylesspalace.tusky.BottomSheetActivity; import com.keylesspalace.tusky.BottomSheetActivity;
import com.keylesspalace.tusky.MainActivity; import com.keylesspalace.tusky.MainActivity;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.PostLookupFallbackBehavior; import com.keylesspalace.tusky.PostLookupFallbackBehavior;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.ViewMediaActivity; import com.keylesspalace.tusky.ViewMediaActivity;
import com.keylesspalace.tusky.ViewTagActivity; import com.keylesspalace.tusky.ViewTagActivity;
import com.keylesspalace.tusky.components.compose.ComposeActivity; import com.keylesspalace.tusky.components.compose.ComposeActivity;
@ -63,6 +60,7 @@ import com.keylesspalace.tusky.entity.PollOption;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.network.TimelineCases; import com.keylesspalace.tusky.network.TimelineCases;
import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.view.MuteAccountDialog; import com.keylesspalace.tusky.view.MuteAccountDialog;
import com.keylesspalace.tusky.viewdata.AttachmentViewData; import com.keylesspalace.tusky.viewdata.AttachmentViewData;
@ -75,9 +73,8 @@ import java.util.regex.Pattern;
import javax.inject.Inject; import javax.inject.Inject;
import kotlin.Unit;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import kotlin.Unit;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.Callback; import retrofit2.Callback;
import retrofit2.Response; import retrofit2.Response;
@ -91,7 +88,7 @@ import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvid
* adapters. I feel like the profile pages and thread viewer, which I haven't made yet, will also * adapters. I feel like the profile pages and thread viewer, which I haven't made yet, will also
* overlap functionality. So, I'm momentarily leaving it and hopefully working on those will clear * overlap functionality. So, I'm momentarily leaving it and hopefully working on those will clear
* up what needs to be where. */ * up what needs to be where. */
public abstract class SFragment extends BaseFragment implements Injectable { public abstract class SFragment extends Fragment implements Injectable {
protected abstract void removeItem(int position); protected abstract void removeItem(int position);
@ -102,7 +99,7 @@ public abstract class SFragment extends BaseFragment implements Injectable {
private static List<Filter> filters; private static List<Filter> filters;
private boolean filterRemoveRegex; private boolean filterRemoveRegex;
private Matcher filterRemoveRegexMatcher; private Matcher filterRemoveRegexMatcher;
private static Matcher alphanumeric = Pattern.compile("^\\w+$").matcher(""); private static final Matcher alphanumeric = Pattern.compile("^\\w+$").matcher("");
@Inject @Inject
public MastodonApi mastodonApi; public MastodonApi mastodonApi;
@ -340,8 +337,8 @@ public abstract class SFragment extends BaseFragment implements Injectable {
MuteAccountDialog.showMuteAccountDialog( MuteAccountDialog.showMuteAccountDialog(
this.getActivity(), this.getActivity(),
accountUsername, accountUsername,
(notifications) -> { (notifications, duration) -> {
timelineCases.mute(accountId, notifications); timelineCases.mute(accountId, notifications, duration);
return Unit.INSTANCE; return Unit.INSTANCE;
} }
); );
@ -395,10 +392,9 @@ public abstract class SFragment extends BaseFragment implements Injectable {
} }
break; break;
} }
default:
case UNKNOWN: { case UNKNOWN: {
/* Intentionally do nothing. This case is here is to handle when new attachment LinkHelper.openLink(active.getUrl(), getContext());
* types are added to the API before code is added here to handle them. So, the
* best fallback is to just show the preview and ignore requests to view them. */
break; break;
} }
} }

View file

@ -24,11 +24,13 @@ import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.accessibility.AccessibilityManager;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.arch.core.util.Function; import androidx.arch.core.util.Function;
import androidx.core.content.ContextCompat;
import androidx.core.util.Pair; import androidx.core.util.Pair;
import androidx.core.widget.ContentLoadingProgressBar; import androidx.core.widget.ContentLoadingProgressBar;
import androidx.lifecycle.Lifecycle; import androidx.lifecycle.Lifecycle;
@ -74,6 +76,7 @@ import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.repository.Placeholder; import com.keylesspalace.tusky.repository.Placeholder;
import com.keylesspalace.tusky.repository.TimelineRepository; import com.keylesspalace.tusky.repository.TimelineRepository;
import com.keylesspalace.tusky.repository.TimelineRequestMode; import com.keylesspalace.tusky.repository.TimelineRequestMode;
import com.keylesspalace.tusky.settings.PrefKeys;
import com.keylesspalace.tusky.util.CardViewMode; import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.Either; import com.keylesspalace.tusky.util.Either;
import com.keylesspalace.tusky.util.HttpHeaderLink; import com.keylesspalace.tusky.util.HttpHeaderLink;
@ -83,7 +86,6 @@ import com.keylesspalace.tusky.util.ListUtils;
import com.keylesspalace.tusky.util.PairedList; import com.keylesspalace.tusky.util.PairedList;
import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.util.StringUtils; import com.keylesspalace.tusky.util.StringUtils;
import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.util.ViewDataUtils; import com.keylesspalace.tusky.util.ViewDataUtils;
import com.keylesspalace.tusky.view.BackgroundMessageView; import com.keylesspalace.tusky.view.BackgroundMessageView;
import com.keylesspalace.tusky.view.EndlessOnScrollListener; import com.keylesspalace.tusky.view.EndlessOnScrollListener;
@ -95,18 +97,18 @@ import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.ListIterator; import java.util.ListIterator;
import java.util.Objects;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.inject.Inject; import javax.inject.Inject;
import at.connyduck.sparkbutton.helpers.Utils; import at.connyduck.sparkbutton.helpers.Utils;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import kotlin.Unit; import kotlin.Unit;
import kotlin.collections.CollectionsKt; import kotlin.collections.CollectionsKt;
import kotlin.jvm.functions.Function1; import kotlin.jvm.functions.Function1;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response; import retrofit2.Response;
import static com.uber.autodispose.AutoDispose.autoDisposable; import static com.uber.autodispose.AutoDispose.autoDisposable;
@ -252,7 +254,9 @@ public class TimelineFragment extends SFragment implements
preferences.getBoolean("showCardsInTimelines", false) ? preferences.getBoolean("showCardsInTimelines", false) ?
CardViewMode.INDENTED : CardViewMode.INDENTED :
CardViewMode.NONE, CardViewMode.NONE,
preferences.getBoolean("confirmReblogs", true) preferences.getBoolean("confirmReblogs", true),
preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
); );
adapter = new TimelineAdapter(dataSource, statusDisplayOptions, this); adapter = new TimelineAdapter(dataSource, statusDisplayOptions, this);
@ -1003,7 +1007,7 @@ public class TimelineFragment extends SFragment implements
} }
} }
private Call<List<Status>> getFetchCallByTimelineType(String fromId, String uptoId) { private Single<Response<List<Status>>> getFetchCallByTimelineType(String fromId, String uptoId) {
MastodonApi api = mastodonApi; MastodonApi api = mastodonApi;
switch (kind) { switch (kind) {
default: default:
@ -1050,37 +1054,31 @@ public class TimelineFragment extends SFragment implements
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
.subscribe( .subscribe(
(result) -> onFetchTimelineSuccess(result, fetchEnd, pos), result -> onFetchTimelineSuccess(result, fetchEnd, pos),
(err) -> onFetchTimelineFailure(new Exception(err), fetchEnd, pos) err -> onFetchTimelineFailure(err, fetchEnd, pos)
); );
} else { } else {
Callback<List<Status>> callback = new Callback<List<Status>>() { getFetchCallByTimelineType(maxId, sinceId)
@Override .observeOn(AndroidSchedulers.mainThread())
public void onResponse(@NonNull Call<List<Status>> call, @NonNull Response<List<Status>> response) { .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
if (response.isSuccessful()) { .subscribe(
@Nullable response -> {
String newNextId = extractNextId(response); if (response.isSuccessful()) {
if (newNextId != null) { @Nullable
// when we reach the bottom of the list, we won't have a new link. If String newNextId = extractNextId(response);
// we blindly write `null` here we will start loading from the top if (newNextId != null) {
// again. // when we reach the bottom of the list, we won't have a new link. If
nextId = newNextId; // we blindly write `null` here we will start loading from the top
} // again.
onFetchTimelineSuccess(liftStatusList(response.body()), fetchEnd, pos); nextId = newNextId;
} else { }
onFetchTimelineFailure(new Exception(response.message()), fetchEnd, pos); onFetchTimelineSuccess(liftStatusList(response.body()), fetchEnd, pos);
} } else {
} onFetchTimelineFailure(new Exception(response.message()), fetchEnd, pos);
}
@Override },
public void onFailure(@NonNull Call<List<Status>> call, @NonNull Throwable t) { err -> onFetchTimelineFailure(err, fetchEnd, pos)
onFetchTimelineFailure((Exception) t, fetchEnd, pos); );
}
};
Call<List<Status>> listCall = getFetchCallByTimelineType(maxId, sinceId);
callList.add(listCall);
listCall.enqueue(callback);
} }
} }
@ -1157,7 +1155,7 @@ public class TimelineFragment extends SFragment implements
} }
} }
private void onFetchTimelineFailure(Exception exception, FetchEnd fetchEnd, int position) { private void onFetchTimelineFailure(Throwable throwable, FetchEnd fetchEnd, int position) {
if (isAdded()) { if (isAdded()) {
swipeRefreshLayout.setRefreshing(false); swipeRefreshLayout.setRefreshing(false);
topProgressBar.hide(); topProgressBar.hide();
@ -1176,7 +1174,7 @@ public class TimelineFragment extends SFragment implements
} else if (this.statuses.isEmpty()) { } else if (this.statuses.isEmpty()) {
swipeRefreshLayout.setEnabled(false); swipeRefreshLayout.setEnabled(false);
this.statusView.setVisibility(View.VISIBLE); this.statusView.setVisibility(View.VISIBLE);
if (exception instanceof IOException) { if (throwable instanceof IOException) {
this.statusView.setup(R.drawable.elephant_offline, R.string.error_network, __ -> { this.statusView.setup(R.drawable.elephant_offline, R.string.error_network, __ -> {
this.progressBar.setVisibility(View.VISIBLE); this.progressBar.setVisibility(View.VISIBLE);
this.onRefresh(); this.onRefresh();
@ -1191,7 +1189,7 @@ public class TimelineFragment extends SFragment implements
} }
} }
Log.e(TAG, "Fetch Failure: " + exception.getMessage()); Log.e(TAG, "Fetch Failure: " + throwable.getMessage());
updateBottomLoadingState(fetchEnd); updateBottomLoadingState(fetchEnd);
progressBar.setVisibility(View.GONE); progressBar.setVisibility(View.GONE);
} }
@ -1476,9 +1474,21 @@ public class TimelineFragment extends SFragment implements
} }
}; };
AccessibilityManager a11yManager;
boolean talkBackWasEnabled;
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
a11yManager = Objects.requireNonNull(
ContextCompat.getSystemService(requireContext(), AccessibilityManager.class)
);
boolean wasEnabled = this.talkBackWasEnabled;
talkBackWasEnabled = a11yManager.isEnabled();
Log.d(TAG, "talkback was enabled: " + wasEnabled + ", now " + talkBackWasEnabled);
if (talkBackWasEnabled && !wasEnabled) {
this.adapter.notifyDataSetChanged();
}
startUpdateTimestamp(); startUpdateTimestamp();
} }

View file

@ -17,10 +17,11 @@ package com.keylesspalace.tusky.fragment
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import androidx.fragment.app.Fragment
import com.keylesspalace.tusky.ViewMediaActivity import com.keylesspalace.tusky.ViewMediaActivity
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
abstract class ViewMediaFragment : BaseFragment() { abstract class ViewMediaFragment : Fragment() {
private var toolbarVisibiltyDisposable: Function0<Boolean>? = null private var toolbarVisibiltyDisposable: Function0<Boolean>? = null
abstract fun setupMediaView( abstract fun setupMediaView(

View file

@ -55,14 +55,13 @@ import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.entity.Filter; import com.keylesspalace.tusky.entity.Filter;
import com.keylesspalace.tusky.entity.Poll; import com.keylesspalace.tusky.entity.Poll;
import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.entity.StatusContext;
import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.settings.PrefKeys;
import com.keylesspalace.tusky.util.CardViewMode; import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate; import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
import com.keylesspalace.tusky.util.PairedList; import com.keylesspalace.tusky.util.PairedList;
import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.util.ViewDataUtils; import com.keylesspalace.tusky.util.ViewDataUtils;
import com.keylesspalace.tusky.view.ConversationLineItemDecoration; import com.keylesspalace.tusky.view.ConversationLineItemDecoration;
import com.keylesspalace.tusky.viewdata.StatusViewData; import com.keylesspalace.tusky.viewdata.StatusViewData;
@ -75,9 +74,6 @@ import java.util.Locale;
import javax.inject.Inject; import javax.inject.Inject;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import static com.uber.autodispose.AutoDispose.autoDisposable; import static com.uber.autodispose.AutoDispose.autoDisposable;
import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from; import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from;
@ -127,6 +123,7 @@ public final class ViewThreadFragment extends SFragment implements
thisThreadsStatusId = getArguments().getString("id"); thisThreadsStatusId = getArguments().getString("id");
SharedPreferences preferences = SharedPreferences preferences =
PreferenceManager.getDefaultSharedPreferences(getActivity()); PreferenceManager.getDefaultSharedPreferences(getActivity());
StatusDisplayOptions statusDisplayOptions = new StatusDisplayOptions( StatusDisplayOptions statusDisplayOptions = new StatusDisplayOptions(
preferences.getBoolean("animateGifAvatars", false), preferences.getBoolean("animateGifAvatars", false),
accountManager.getActiveAccount().getMediaPreviewEnabled(), accountManager.getActiveAccount().getMediaPreviewEnabled(),
@ -136,7 +133,9 @@ public final class ViewThreadFragment extends SFragment implements
preferences.getBoolean("showCardsInTimelines", false) ? preferences.getBoolean("showCardsInTimelines", false) ?
CardViewMode.INDENTED : CardViewMode.INDENTED :
CardViewMode.NONE, CardViewMode.NONE,
preferences.getBoolean("confirmReblogs", true) preferences.getBoolean("confirmReblogs", true),
preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
); );
adapter = new ThreadAdapter(statusDisplayOptions, this); adapter = new ThreadAdapter(statusDisplayOptions, this);
} }
@ -461,49 +460,32 @@ public final class ViewThreadFragment extends SFragment implements
} }
private void sendStatusRequest(final String id) { private void sendStatusRequest(final String id) {
Call<Status> call = mastodonApi.status(id); mastodonApi.status(id)
call.enqueue(new Callback<Status>() { .observeOn(AndroidSchedulers.mainThread())
@Override .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) { .subscribe(
if (response.isSuccessful()) { status -> {
int position = setStatus(response.body()); int position = setStatus(status);
recyclerView.scrollToPosition(position); recyclerView.scrollToPosition(position);
} else { },
onThreadRequestFailure(id); throwable -> onThreadRequestFailure(id, throwable)
} );
}
@Override
public void onFailure(@NonNull Call<Status> call, @NonNull Throwable t) {
onThreadRequestFailure(id);
}
});
callList.add(call);
} }
private void sendThreadRequest(final String id) { private void sendThreadRequest(final String id) {
Call<StatusContext> call = mastodonApi.statusContext(id); mastodonApi.statusContext(id)
call.enqueue(new Callback<StatusContext>() { .observeOn(AndroidSchedulers.mainThread())
@Override .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
public void onResponse(@NonNull Call<StatusContext> call, @NonNull Response<StatusContext> response) { .subscribe(
StatusContext context = response.body(); context -> {
if (response.isSuccessful() && context != null) { swipeRefreshLayout.setRefreshing(false);
swipeRefreshLayout.setRefreshing(false); setContext(context.getAncestors(), context.getDescendants());
setContext(context.getAncestors(), context.getDescendants()); },
} else { throwable -> onThreadRequestFailure(id, throwable)
onThreadRequestFailure(id); );
}
}
@Override
public void onFailure(@NonNull Call<StatusContext> call, @NonNull Throwable t) {
onThreadRequestFailure(id);
}
});
callList.add(call);
} }
private void onThreadRequestFailure(final String id) { private void onThreadRequestFailure(final String id, final Throwable throwable) {
View view = getView(); View view = getView();
swipeRefreshLayout.setRefreshing(false); swipeRefreshLayout.setRefreshing(false);
if (view != null) { if (view != null) {
@ -514,7 +496,7 @@ public final class ViewThreadFragment extends SFragment implements
}) })
.show(); .show();
} else { } else {
Log.e(TAG, "Couldn't display thread fetch error message"); Log.e(TAG, "Network request failed", throwable);
} }
} }

View file

@ -56,14 +56,7 @@ interface MastodonApi {
@Query("max_id") maxId: String?, @Query("max_id") maxId: String?,
@Query("since_id") sinceId: String?, @Query("since_id") sinceId: String?,
@Query("limit") limit: Int? @Query("limit") limit: Int?
): Call<List<Status>> ): Single<Response<List<Status>>>
@GET("api/v1/timelines/home")
fun homeTimelineSingle(
@Query("max_id") maxId: String?,
@Query("since_id") sinceId: String?,
@Query("limit") limit: Int?
): Single<List<Status>>
@GET("api/v1/timelines/public") @GET("api/v1/timelines/public")
fun publicTimeline( fun publicTimeline(
@ -71,7 +64,7 @@ interface MastodonApi {
@Query("max_id") maxId: String?, @Query("max_id") maxId: String?,
@Query("since_id") sinceId: String?, @Query("since_id") sinceId: String?,
@Query("limit") limit: Int? @Query("limit") limit: Int?
): Call<List<Status>> ): Single<Response<List<Status>>>
@GET("api/v1/timelines/tag/{hashtag}") @GET("api/v1/timelines/tag/{hashtag}")
fun hashtagTimeline( fun hashtagTimeline(
@ -81,7 +74,7 @@ interface MastodonApi {
@Query("max_id") maxId: String?, @Query("max_id") maxId: String?,
@Query("since_id") sinceId: String?, @Query("since_id") sinceId: String?,
@Query("limit") limit: Int? @Query("limit") limit: Int?
): Call<List<Status>> ): Single<Response<List<Status>>>
@GET("api/v1/timelines/list/{listId}") @GET("api/v1/timelines/list/{listId}")
fun listTimeline( fun listTimeline(
@ -89,7 +82,7 @@ interface MastodonApi {
@Query("max_id") maxId: String?, @Query("max_id") maxId: String?,
@Query("since_id") sinceId: String?, @Query("since_id") sinceId: String?,
@Query("limit") limit: Int? @Query("limit") limit: Int?
): Call<List<Status>> ): Single<Response<List<Status>>>
@GET("api/v1/notifications") @GET("api/v1/notifications")
fun notifications( fun notifications(
@ -97,7 +90,7 @@ interface MastodonApi {
@Query("since_id") sinceId: String?, @Query("since_id") sinceId: String?,
@Query("limit") limit: Int?, @Query("limit") limit: Int?,
@Query("exclude_types[]") excludes: Set<Notification.Type>? @Query("exclude_types[]") excludes: Set<Notification.Type>?
): Call<List<Notification>> ): Single<Response<List<Notification>>>
@GET("api/v1/markers") @GET("api/v1/markers")
fun markersWithAuth( fun markersWithAuth(
@ -114,17 +107,13 @@ interface MastodonApi {
): Single<List<Notification>> ): Single<List<Notification>>
@POST("api/v1/notifications/clear") @POST("api/v1/notifications/clear")
fun clearNotifications(): Call<ResponseBody> fun clearNotifications(): Single<ResponseBody>
@GET("api/v1/notifications/{id}")
fun notification(
@Path("id") notificationId: String
): Call<Notification>
@Multipart @Multipart
@POST("api/v1/media") @POST("api/v1/media")
fun uploadMedia( fun uploadMedia(
@Part file: MultipartBody.Part @Part file: MultipartBody.Part,
@Part description: MultipartBody.Part? = null
): Single<Attachment> ): Single<Attachment>
@FormUrlEncoded @FormUrlEncoded
@ -145,12 +134,12 @@ interface MastodonApi {
@GET("api/v1/statuses/{id}") @GET("api/v1/statuses/{id}")
fun status( fun status(
@Path("id") statusId: String @Path("id") statusId: String
): Call<Status> ): Single<Status>
@GET("api/v1/statuses/{id}/context") @GET("api/v1/statuses/{id}/context")
fun statusContext( fun statusContext(
@Path("id") statusId: String @Path("id") statusId: String
): Call<StatusContext> ): Single<StatusContext>
@GET("api/v1/statuses/{id}/reblogged_by") @GET("api/v1/statuses/{id}/reblogged_by")
fun statusRebloggedBy( fun statusRebloggedBy(
@ -289,7 +278,7 @@ interface MastodonApi {
@Query("exclude_replies") excludeReplies: Boolean?, @Query("exclude_replies") excludeReplies: Boolean?,
@Query("only_media") onlyMedia: Boolean?, @Query("only_media") onlyMedia: Boolean?,
@Query("pinned") pinned: Boolean? @Query("pinned") pinned: Boolean?
): Call<List<Status>> ): Single<Response<List<Status>>>
@GET("api/v1/accounts/{id}/followers") @GET("api/v1/accounts/{id}/followers")
fun accountFollowers( fun accountFollowers(
@ -307,7 +296,8 @@ interface MastodonApi {
@POST("api/v1/accounts/{id}/follow") @POST("api/v1/accounts/{id}/follow")
fun followAccount( fun followAccount(
@Path("id") accountId: String, @Path("id") accountId: String,
@Field("reblogs") showReblogs: Boolean @Field("reblogs") showReblogs: Boolean? = null,
@Field("notify") notify: Boolean? = null
): Single<Relationship> ): Single<Relationship>
@POST("api/v1/accounts/{id}/unfollow") @POST("api/v1/accounts/{id}/unfollow")
@ -329,7 +319,8 @@ interface MastodonApi {
@POST("api/v1/accounts/{id}/mute") @POST("api/v1/accounts/{id}/mute")
fun muteAccount( fun muteAccount(
@Path("id") accountId: String, @Path("id") accountId: String,
@Field("notifications") notifications: Boolean? = null @Field("notifications") notifications: Boolean? = null,
@Field("duration") duration: Int? = null
): Single<Relationship> ): Single<Relationship>
@POST("api/v1/accounts/{id}/unmute") @POST("api/v1/accounts/{id}/unmute")
@ -347,6 +338,16 @@ interface MastodonApi {
@Path("id") accountId: String @Path("id") accountId: String
): Single<List<IdentityProof>> ): Single<List<IdentityProof>>
@POST("api/v1/pleroma/accounts/{id}/subscribe")
fun subscribeAccount(
@Path("id") accountId: String
): Single<Relationship>
@POST("api/v1/pleroma/accounts/{id}/unsubscribe")
fun unsubscribeAccount(
@Path("id") accountId: String
): Single<Relationship>
@GET("api/v1/blocks") @GET("api/v1/blocks")
fun blocks( fun blocks(
@Query("max_id") maxId: String? @Query("max_id") maxId: String?
@ -380,14 +381,14 @@ interface MastodonApi {
@Query("max_id") maxId: String?, @Query("max_id") maxId: String?,
@Query("since_id") sinceId: String?, @Query("since_id") sinceId: String?,
@Query("limit") limit: Int? @Query("limit") limit: Int?
): Call<List<Status>> ): Single<Response<List<Status>>>
@GET("api/v1/bookmarks") @GET("api/v1/bookmarks")
fun bookmarks( fun bookmarks(
@Query("max_id") maxId: String?, @Query("max_id") maxId: String?,
@Query("since_id") sinceId: String?, @Query("since_id") sinceId: String?,
@Query("limit") limit: Int? @Query("limit") limit: Int?
): Call<List<Status>> ): Single<Response<List<Status>>>
@GET("api/v1/follow_requests") @GET("api/v1/follow_requests")
fun followRequests( fun followRequests(
@ -397,20 +398,10 @@ interface MastodonApi {
@POST("api/v1/follow_requests/{id}/authorize") @POST("api/v1/follow_requests/{id}/authorize")
fun authorizeFollowRequest( fun authorizeFollowRequest(
@Path("id") accountId: String @Path("id") accountId: String
): Call<Relationship>
@POST("api/v1/follow_requests/{id}/reject")
fun rejectFollowRequest(
@Path("id") accountId: String
): Call<Relationship>
@POST("api/v1/follow_requests/{id}/authorize")
fun authorizeFollowRequestObservable(
@Path("id") accountId: String
): Single<Relationship> ): Single<Relationship>
@POST("api/v1/follow_requests/{id}/reject") @POST("api/v1/follow_requests/{id}/reject")
fun rejectFollowRequestObservable( fun rejectFollowRequest(
@Path("id") accountId: String @Path("id") accountId: String
): Single<Relationship> ): Single<Relationship>

View file

@ -33,7 +33,7 @@ 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 bookmark(status: Status, bookmark: Boolean): Single<Status>
fun mute(id: String, notifications: Boolean) fun mute(id: String, notifications: Boolean, duration: Int)
fun block(id: String) fun block(id: String)
fun delete(id: String): Single<DeletedStatus> fun delete(id: String): Single<DeletedStatus>
fun pin(status: Status, pin: Boolean) fun pin(status: Status, pin: Boolean)
@ -104,8 +104,8 @@ class TimelineCasesImpl(
} }
} }
override fun mute(id: String, notifications: Boolean) { override fun mute(id: String, notifications: Boolean, duration: Int) {
mastodonApi.muteAccount(id, notifications) mastodonApi.muteAccount(id, notifications, duration)
.subscribe({ .subscribe({
eventHub.dispatch(MuteEvent(id)) eventHub.dispatch(MuteEvent(id))
}, { t -> }, { t ->

View file

@ -60,7 +60,6 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
val notificationManager = NotificationManagerCompat.from(context) val notificationManager = NotificationManagerCompat.from(context)
if (intent.action == NotificationHelper.REPLY_ACTION) { if (intent.action == NotificationHelper.REPLY_ACTION) {
val message = getReplyMessage(intent) val message = getReplyMessage(intent)
@ -89,22 +88,23 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
val sendIntent = SendTootService.sendTootIntent( val sendIntent = SendTootService.sendTootIntent(
context, context,
TootToSend( TootToSend(
text, text = text,
spoiler, warningText = spoiler,
visibility.serverString(), visibility = visibility.serverString(),
false, sensitive = false,
emptyList(), mediaIds = emptyList(),
emptyList(), mediaUris = emptyList(),
emptyList(), mediaDescriptions = emptyList(),
null, scheduledAt = null,
citedStatusId, inReplyToId = citedStatusId,
null, poll = null,
null, replyingStatusContent = null,
null, replyingStatusAuthorUsername = null,
null, account.id, accountId = account.id,
0, savedTootUid = -1,
randomAlphanumericString(16), draftId = -1,
0 idempotencyKey = randomAlphanumericString(16),
retries = 0
) )
) )

View file

@ -66,9 +66,9 @@ class TimelineRepositoryImpl(
sinceIdMinusOne: String?, limit: Int, sinceIdMinusOne: String?, limit: Int,
accountId: Long, requestMode: TimelineRequestMode accountId: Long, requestMode: TimelineRequestMode
): Single<out List<TimelineStatus>> { ): Single<out List<TimelineStatus>> {
return mastodonApi.homeTimelineSingle(maxId, sinceIdMinusOne, limit + 1) return mastodonApi.homeTimeline(maxId, sinceIdMinusOne, limit + 1)
.map { statuses -> .map { response ->
this.saveStatusesToDb(accountId, statuses, maxId, sinceId) this.saveStatusesToDb(accountId, response.body().orEmpty(), maxId, sinceId)
} }
.flatMap { statuses -> .flatMap { statuses ->
this.addFromDbIfNeeded(accountId, statuses, maxId, sinceId, limit, requestMode) this.addFromDbIfNeeded(accountId, statuses, maxId, sinceId, limit, requestMode)
@ -85,7 +85,7 @@ class TimelineRepositoryImpl(
private fun addFromDbIfNeeded(accountId: Long, statuses: List<Either<Placeholder, Status>>, private fun addFromDbIfNeeded(accountId: Long, statuses: List<Either<Placeholder, Status>>,
maxId: String?, sinceId: String?, limit: Int, maxId: String?, sinceId: String?, limit: Int,
requestMode: TimelineRequestMode requestMode: TimelineRequestMode
): Single<List<TimelineStatus>>? { ): Single<List<TimelineStatus>> {
return if (requestMode != NETWORK && statuses.size < 2) { return if (requestMode != NETWORK && statuses.size < 2) {
val newMaxID = if (statuses.isEmpty()) { val newMaxID = if (statuses.isEmpty()) {
maxId maxId

View file

@ -18,6 +18,7 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.StatusComposedEvent import com.keylesspalace.tusky.appstore.StatusComposedEvent
import com.keylesspalace.tusky.appstore.StatusScheduledEvent import com.keylesspalace.tusky.appstore.StatusScheduledEvent
import com.keylesspalace.tusky.components.drafts.DraftHelper
import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
@ -46,7 +47,8 @@ class SendTootService : Service(), Injectable {
lateinit var eventHub: EventHub lateinit var eventHub: EventHub
@Inject @Inject
lateinit var database: AppDatabase lateinit var database: AppDatabase
@Inject
lateinit var draftHelper: DraftHelper
@Inject @Inject
lateinit var saveTootHelper: SaveTootHelper lateinit var saveTootHelper: SaveTootHelper
@ -163,6 +165,10 @@ class SendTootService : Service(), Injectable {
if (tootToSend.savedTootUid != 0) { if (tootToSend.savedTootUid != 0) {
saveTootHelper.deleteDraft(tootToSend.savedTootUid) saveTootHelper.deleteDraft(tootToSend.savedTootUid)
} }
if (tootToSend.draftId != 0) {
draftHelper.deleteDraftAndAttachments(tootToSend.draftId)
.subscribe()
}
if (scheduled) { if (scheduled) {
response.body()?.let(::StatusScheduledEvent)?.let(eventHub::dispatch) response.body()?.let(::StatusScheduledEvent)?.let(eventHub::dispatch)
@ -245,17 +251,19 @@ class SendTootService : Service(), Injectable {
private fun saveTootToDrafts(toot: TootToSend) { private fun saveTootToDrafts(toot: TootToSend) {
saveTootHelper.saveToot(toot.text, draftHelper.saveDraft(
toot.warningText, draftId = toot.draftId,
toot.savedJsonUrls, accountId = toot.accountId,
toot.mediaUris, inReplyToId = toot.inReplyToId,
toot.mediaDescriptions, content = toot.text,
toot.savedTootUid, contentWarning = toot.warningText,
toot.inReplyToId, sensitive = toot.sensitive,
toot.replyingStatusContent, visibility = Status.Visibility.byString(toot.visibility),
toot.replyingStatusAuthorUsername, mediaUris = toot.mediaUris,
Status.Visibility.byString(toot.visibility), mediaDescriptions = toot.mediaDescriptions,
toot.poll) poll = toot.poll,
failedToSend = true
).subscribe()
} }
private fun cancelSendingIntent(tootId: Int): PendingIntent { private fun cancelSendingIntent(tootId: Int): PendingIntent {
@ -323,9 +331,9 @@ data class TootToSend(
val poll: NewPoll?, val poll: NewPoll?,
val replyingStatusContent: String?, val replyingStatusContent: String?,
val replyingStatusAuthorUsername: String?, val replyingStatusAuthorUsername: String?,
val savedJsonUrls: List<String>?,
val accountId: Long, val accountId: Long,
val savedTootUid: Int, val savedTootUid: Int,
val draftId: Int,
val idempotencyKey: String, val idempotencyKey: String,
var retries: Int var retries: Int
) : Parcelable ) : Parcelable

View file

@ -31,8 +31,12 @@ object PrefKeys {
const val SHOW_CARDS_IN_TIMELINES = "showCardsInTimelines" const val SHOW_CARDS_IN_TIMELINES = "showCardsInTimelines"
const val CONFIRM_REBLOGS = "confirmReblogs" const val CONFIRM_REBLOGS = "confirmReblogs"
const val ENABLE_SWIPE_FOR_TABS = "enableSwipeForTabs" const val ENABLE_SWIPE_FOR_TABS = "enableSwipeForTabs"
const val ANIMATE_CUSTOM_EMOJIS = "animateCustomEmojis"
const val CUSTOM_TABS = "customTabs" const val CUSTOM_TABS = "customTabs"
const val WELLBEING_LIMITED_NOTIFICATIONS = "wellbeingModeLimitedNotifications"
const val WELLBEING_HIDE_STATS_POSTS = "wellbeingHideStatsPosts"
const val WELLBEING_HIDE_STATS_PROFILE = "wellbeingHideStatsProfile"
const val HTTP_PROXY_ENABLED = "httpProxyEnabled" const val HTTP_PROXY_ENABLED = "httpProxyEnabled"
const val HTTP_PROXY_SERVER = "httpProxyServer" const val HTTP_PROXY_SERVER = "httpProxyServer"
@ -53,6 +57,7 @@ object PrefKeys {
const val NOTIFICATION_FILTER_REBLOGS = "notificationFilterReblogs" const val NOTIFICATION_FILTER_REBLOGS = "notificationFilterReblogs"
const val NOTIFICATION_FILTER_FOLLOW_REQUESTS = "notificationFilterFollowRequests" const val NOTIFICATION_FILTER_FOLLOW_REQUESTS = "notificationFilterFollowRequests"
const val NOTIFICATIONS_FILTER_FOLLOWS = "notificationFilterFollows" const val NOTIFICATIONS_FILTER_FOLLOWS = "notificationFilterFollows"
const val NOTIFICATION_FILTER_SUBSCRIPTIONS = "notificationFilterSubscriptions"
const val TAB_FILTER_HOME_REPLIES = "tabFilterHomeReplies" const val TAB_FILTER_HOME_REPLIES = "tabFilterHomeReplies"
const val TAB_FILTER_HOME_BOOSTS = "tabFilterHomeBoosts" const val TAB_FILTER_HOME_BOOSTS = "tabFilterHomeBoosts"

View file

@ -0,0 +1,8 @@
package com.keylesspalace.tusky.util
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
class BindingViewHolder<T : ViewBinding>(
val binding: T
) : RecyclerView.ViewHolder(binding.root)

View file

@ -21,20 +21,52 @@ import android.text.TextUtils
import android.widget.MultiAutoCompleteTextView import android.widget.MultiAutoCompleteTextView
class ComposeTokenizer : MultiAutoCompleteTextView.Tokenizer { class ComposeTokenizer : MultiAutoCompleteTextView.Tokenizer {
private fun isMentionOrHashtagAllowedCharacter(character: Char) : Boolean {
return Character.isLetterOrDigit(character) || character == '_' // simple usernames
|| character == '-' // extended usernames
|| character == '.' // domain dot
}
override fun findTokenStart(text: CharSequence, cursor: Int): Int { override fun findTokenStart(text: CharSequence, cursor: Int): Int {
if (cursor == 0) { if (cursor == 0) {
return cursor return cursor
} }
var i = cursor var i = cursor
var character = text[i - 1] var character = text[i - 1]
while (i > 0 && character != '@' && character != '#' && character != ':') {
// See SpanUtils.MENTION_REGEX // go up to first illegal character or character we're looking for (@, # or :)
if (!Character.isLetterOrDigit(character) && character != '_') { while(i > 0 && !(character == '@' || character == '#' || character == ':')) {
if(!isMentionOrHashtagAllowedCharacter(character)) {
return cursor return cursor
} }
i-- i--
character = if (i == 0) ' ' else text[i - 1] character = if (i == 0) ' ' else text[i - 1]
} }
// maybe caught domain name? try search username
if(i > 2 && character == '@') {
var j = i - 1
var character2 = text[i - 2]
// again go up to first illegal character or tag "@"
while(j > 0 && character2 != '@') {
if(!isMentionOrHashtagAllowedCharacter(character2)) {
break
}
j--
character2 = if (j == 0) ' ' else text[j - 1]
}
// found mention symbol, override cursor
if(character2 == '@') {
i = j
character = character2
}
}
if (i < 1 if (i < 1
|| (character != '@' && character != '#' && character != ':') || (character != '@' && character != '#' && character != ':')
|| i > 1 && !Character.isWhitespace(text[i - 2])) { || i > 1 && !Character.isWhitespace(text[i - 2])) {

View file

@ -16,11 +16,9 @@
@file:JvmName("CustomEmojiHelper") @file:JvmName("CustomEmojiHelper")
package com.keylesspalace.tusky.util package com.keylesspalace.tusky.util
import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Paint import android.graphics.Paint
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.*
import android.graphics.drawable.Drawable
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.style.ReplacementSpan import android.text.style.ReplacementSpan
import android.view.View import android.view.View
@ -33,6 +31,8 @@ import com.keylesspalace.tusky.entity.Emoji
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.regex.Pattern import java.util.regex.Pattern
import androidx.preference.PreferenceManager
import com.keylesspalace.tusky.settings.PrefKeys
/** /**
* replaces emoji shortcodes in a text with EmojiSpans * replaces emoji shortcodes in a text with EmojiSpans
@ -41,7 +41,7 @@ import java.util.regex.Pattern
* @param view a reference to the a view the emojis will be shown in (should be the TextView, but parents of the TextView are also acceptable) * @param view a reference to the a view the emojis will be shown in (should be the TextView, but parents of the TextView are also acceptable)
* @return the text with the shortcodes replaced by EmojiSpans * @return the text with the shortcodes replaced by EmojiSpans
*/ */
fun CharSequence.emojify(emojis: List<Emoji>?, view: View) : CharSequence { fun CharSequence.emojify(emojis: List<Emoji>?, view: View, animate: Boolean) : CharSequence {
if(emojis.isNullOrEmpty()) if(emojis.isNullOrEmpty())
return this return this
@ -56,9 +56,9 @@ fun CharSequence.emojify(emojis: List<Emoji>?, view: View) : CharSequence {
builder.setSpan(span, matcher.start(), matcher.end(), 0) builder.setSpan(span, matcher.start(), matcher.end(), 0)
Glide.with(view) Glide.with(view)
.asBitmap() .asDrawable()
.load(url) .load(url)
.into(span.getTarget()) .into(span.getTarget(animate))
} }
} }
return builder return builder
@ -97,11 +97,29 @@ class EmojiSpan(val viewWeakReference: WeakReference<View>) : ReplacementSpan()
} }
} }
fun getTarget(): Target<Bitmap> { fun getTarget(animate : Boolean): Target<Drawable> {
return object : CustomTarget<Bitmap>() { return object : CustomTarget<Drawable>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) { override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
viewWeakReference.get()?.let { view -> viewWeakReference.get()?.let { view ->
imageDrawable = BitmapDrawable(view.context.resources, resource) if(animate && resource is Animatable) {
val callback = resource.callback
resource.callback = object: Drawable.Callback {
override fun unscheduleDrawable(p0: Drawable, p1: Runnable) {
callback?.unscheduleDrawable(p0, p1)
}
override fun scheduleDrawable(p0: Drawable, p1: Runnable, p2: Long) {
callback?.scheduleDrawable(p0, p1, p2)
}
override fun invalidateDrawable(p0: Drawable) {
callback?.invalidateDrawable(p0)
view.invalidate()
}
}
resource.start()
}
imageDrawable = resource
view.invalidate() view.invalidate()
} }
} }

View file

@ -19,7 +19,6 @@ import android.content.ActivityNotFoundException;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.Spanned; import android.text.Spanned;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
@ -31,6 +30,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabColorSchemeParams;
import androidx.browser.customtabs.CustomTabsIntent; import androidx.browser.customtabs.CustomTabsIntent;
import androidx.preference.PreferenceManager; import androidx.preference.PreferenceManager;
@ -229,18 +229,20 @@ public class LinkHelper {
*/ */
public static void openLinkInCustomTab(Uri uri, Context context) { public static void openLinkInCustomTab(Uri uri, Context context) {
int toolbarColor = ThemeUtils.getColor(context, R.attr.colorSurface); int toolbarColor = ThemeUtils.getColor(context, R.attr.colorSurface);
int navigationbarColor = ThemeUtils.getColor(context, android.R.attr.navigationBarColor);
int navigationbarDividerColor = ThemeUtils.getColor(context, R.attr.dividerColor);
CustomTabsIntent.Builder customTabsIntentBuilder = new CustomTabsIntent.Builder() CustomTabColorSchemeParams colorSchemeParams = new CustomTabColorSchemeParams.Builder()
.setToolbarColor(toolbarColor) .setToolbarColor(toolbarColor)
.setShowTitle(true); .setNavigationBarColor(navigationbarColor)
.setNavigationBarDividerColor(navigationbarDividerColor)
.build();
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder()
customTabsIntentBuilder.setNavigationBarColor( .setDefaultColorSchemeParams(colorSchemeParams)
ThemeUtils.getColor(context, android.R.attr.navigationBarColor) .setShowTitle(true)
); .build();
}
CustomTabsIntent customTabsIntent = customTabsIntentBuilder.build();
try { try {
customTabsIntent.launchUrl(context, uri); customTabsIntent.launchUrl(context, uri);
} catch (ActivityNotFoundException e) { } catch (ActivityNotFoundException e) {

Some files were not shown because too many files have changed in this diff Show more