Merge Tusky 14
This commit is contained in:
commit
76782ca4b3
210 changed files with 6117 additions and 2249 deletions
|
@ -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
|
||||||
|
|
4
app/proguard-rules.pro
vendored
4
app/proguard-rules.pro
vendored
|
@ -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
|
||||||
|
|
747
app/schemas/com.keylesspalace.tusky.db.AppDatabase/24.json
Normal file
747
app/schemas/com.keylesspalace.tusky.db.AppDatabase/24.json
Normal 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')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
821
app/schemas/com.keylesspalace.tusky.db.AppDatabase/25.json
Normal file
821
app/schemas/com.keylesspalace.tusky.db.AppDatabase/25.json
Normal 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')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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());
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
44
app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt
Normal file
44
app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt
Normal 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?>
|
||||||
|
|
||||||
|
}
|
55
app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt
Normal file
55
app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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?,
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 -> {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
|
@ -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])) {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
Loading…
Reference in a new issue