Tab customization & direct messages tab (#1012)
* custom tabs * custom tabs interface * implement custom tab functionality * add database migration * fix bugs, improve ThemeUtils nullability handling * implement conversationsfragment * setup ConversationViewHolder * implement favs * add button functionality * revert 10.json * revert item_status_notification.xml * implement more menu, replying, fix stuff, clean up * fix tests * fix bug with expanding statuses * min and max number of tabs * settings support, fix bugs * database migration * fix scrolling to top after refresh * fix bugs * fix warning in item_conversation
This commit is contained in:
parent
adf573646e
commit
e371fa0e24
75 changed files with 3663 additions and 296 deletions
|
@ -137,4 +137,5 @@ dependencies {
|
|||
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.5.0'
|
||||
implementation 'com.uber.autodispose:autodispose-android-archcomponents:1.1.0'
|
||||
implementation 'com.uber.autodispose:autodispose-ktx:1.1.0'
|
||||
implementation 'androidx.paging:paging-runtime-ktx:2.1.0-rc01'
|
||||
}
|
||||
|
|
668
app/schemas/com.keylesspalace.tusky.db.AppDatabase/12.json
Normal file
668
app/schemas/com.keylesspalace.tusky.db.AppDatabase/12.json
Normal file
|
@ -0,0 +1,668 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 12,
|
||||
"identityHash": "d4d3d4c683ab7f681459b9edab92301c",
|
||||
"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)",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"uid"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "AccountEntity",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "domain",
|
||||
"columnName": "domain",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accessToken",
|
||||
"columnName": "accessToken",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isActive",
|
||||
"columnName": "isActive",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accountId",
|
||||
"columnName": "accountId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "username",
|
||||
"columnName": "username",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "displayName",
|
||||
"columnName": "displayName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "profilePictureUrl",
|
||||
"columnName": "profilePictureUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationsEnabled",
|
||||
"columnName": "notificationsEnabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationsMentioned",
|
||||
"columnName": "notificationsMentioned",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationsFollowed",
|
||||
"columnName": "notificationsFollowed",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationsReblogged",
|
||||
"columnName": "notificationsReblogged",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "notificationsFavorited",
|
||||
"columnName": "notificationsFavorited",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "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": "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
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id"
|
||||
],
|
||||
"autoGenerate": true
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_AccountEntity_domain_accountId",
|
||||
"unique": true,
|
||||
"columnNames": [
|
||||
"domain",
|
||||
"accountId"
|
||||
],
|
||||
"createSql": "CREATE UNIQUE INDEX `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, 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
|
||||
}
|
||||
],
|
||||
"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, `instance` 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, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "serverId",
|
||||
"columnName": "serverId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "url",
|
||||
"columnName": "url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "timelineUserId",
|
||||
"columnName": "timelineUserId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "authorServerId",
|
||||
"columnName": "authorServerId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance",
|
||||
"columnName": "instance",
|
||||
"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": "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
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"serverId",
|
||||
"timelineUserId"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"authorServerId",
|
||||
"timelineUserId"
|
||||
],
|
||||
"createSql": "CREATE INDEX `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, `instance` TEXT 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, PRIMARY KEY(`serverId`, `timelineUserId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "serverId",
|
||||
"columnName": "serverId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "timelineUserId",
|
||||
"columnName": "timelineUserId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "instance",
|
||||
"columnName": "instance",
|
||||
"affinity": "TEXT",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"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_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, 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.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
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"id",
|
||||
"accountId"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"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, \"d4d3d4c683ab7f681459b9edab92301c\")"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -97,6 +97,7 @@
|
|||
<activity android:name=".FavouritesActivity" />
|
||||
<activity android:name=".AccountListActivity" />
|
||||
<activity android:name=".AboutActivity" />
|
||||
<activity android:name=".TabPreferenceActivity" />
|
||||
<activity
|
||||
android:name=".ReportActivity"
|
||||
android:windowSoftInputMode="stateVisible|adjustResize" />
|
||||
|
|
|
@ -85,7 +85,7 @@ abstract class BottomSheetActivity : BaseActivity() {
|
|||
val searchResult = response.body()
|
||||
if(searchResult != null) {
|
||||
if (searchResult.statuses.isNotEmpty()) {
|
||||
viewThread(searchResult.statuses[0])
|
||||
viewThread(searchResult.statuses[0].id, searchResult.statuses[0].url)
|
||||
return
|
||||
} else if (searchResult.accounts.isNotEmpty()) {
|
||||
viewAccount(searchResult.accounts[0].id)
|
||||
|
@ -107,11 +107,11 @@ abstract class BottomSheetActivity : BaseActivity() {
|
|||
onBeginSearch(url)
|
||||
}
|
||||
|
||||
open fun viewThread(status: Status) {
|
||||
open fun viewThread(statusId: String, url: String?) {
|
||||
if (!isSearching()) {
|
||||
val intent = Intent(this, ViewThreadActivity::class.java)
|
||||
intent.putExtra("id", status.actionableId)
|
||||
intent.putExtra("url", status.actionableStatus.url)
|
||||
intent.putExtra("id", statusId)
|
||||
intent.putExtra("url", url)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,12 +8,10 @@ import android.view.MenuItem
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.LoadingState.*
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.entity.MastoList
|
||||
|
|
|
@ -38,11 +38,12 @@ import android.widget.ImageView;
|
|||
|
||||
import com.keylesspalace.tusky.appstore.CacheUpdater;
|
||||
import com.keylesspalace.tusky.appstore.EventHub;
|
||||
import com.keylesspalace.tusky.appstore.MainTabsChangedEvent;
|
||||
import com.keylesspalace.tusky.appstore.ProfileEditedEvent;
|
||||
import com.keylesspalace.tusky.db.AccountEntity;
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
||||
import com.keylesspalace.tusky.pager.TimelinePagerAdapter;
|
||||
import com.keylesspalace.tusky.pager.MainPagerAdapter;
|
||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||
import com.keylesspalace.tusky.util.NotificationHelper;
|
||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||
|
@ -106,22 +107,17 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
|||
private FloatingActionButton composeButton;
|
||||
private AccountHeader headerResult;
|
||||
private Drawer drawer;
|
||||
private TabLayout tabLayout;
|
||||
private ViewPager viewPager;
|
||||
|
||||
private void forwardShare(Intent intent) {
|
||||
Intent composeIntent = new Intent(this, ComposeActivity.class);
|
||||
composeIntent.setAction(intent.getAction());
|
||||
composeIntent.setType(intent.getType());
|
||||
composeIntent.putExtras(intent);
|
||||
startActivity(composeIntent);
|
||||
}
|
||||
private int notificationTabPosition;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Intent intent = getIntent();
|
||||
int tabPosition = 0;
|
||||
boolean showNotificationTab = false;
|
||||
|
||||
if (intent != null) {
|
||||
long accountId = intent.getLongExtra(NotificationHelper.ACCOUNT_ID, -1);
|
||||
|
@ -156,14 +152,14 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
|||
}
|
||||
} else if (accountRequested) {
|
||||
// user clicked a notification, show notification tab and switch user if necessary
|
||||
tabPosition = 1;
|
||||
showNotificationTab = true;
|
||||
}
|
||||
}
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
composeButton = findViewById(R.id.floating_btn);
|
||||
ImageButton drawerToggle = findViewById(R.id.drawer_toggle);
|
||||
TabLayout tabLayout = findViewById(R.id.tab_layout);
|
||||
tabLayout = findViewById(R.id.tab_layout);
|
||||
viewPager = findViewById(R.id.pager);
|
||||
|
||||
composeButton.setOnClickListener(v -> {
|
||||
|
@ -181,60 +177,26 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
|||
* drawer, though, because its callback touches the header in the drawer. */
|
||||
fetchUserInfo();
|
||||
|
||||
// Setup the tabs and timeline pager.
|
||||
TimelinePagerAdapter adapter = new TimelinePagerAdapter(getSupportFragmentManager());
|
||||
setupTabs(showNotificationTab);
|
||||
|
||||
int pageMargin = getResources().getDimensionPixelSize(R.dimen.tab_page_margin);
|
||||
viewPager.setPageMargin(pageMargin);
|
||||
Drawable pageMarginDrawable = ThemeUtils.getDrawable(this, R.attr.tab_page_margin_drawable,
|
||||
R.drawable.tab_page_margin_dark);
|
||||
viewPager.setPageMarginDrawable(pageMarginDrawable);
|
||||
viewPager.setAdapter(adapter);
|
||||
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
|
||||
int[] tabIcons = {
|
||||
R.drawable.ic_home_24dp,
|
||||
R.drawable.ic_notifications_24dp,
|
||||
R.drawable.ic_local_24dp,
|
||||
R.drawable.ic_public_24dp,
|
||||
};
|
||||
String[] pageTitles = {
|
||||
getString(R.string.title_home),
|
||||
getString(R.string.title_notifications),
|
||||
getString(R.string.title_public_local),
|
||||
getString(R.string.title_public_federated),
|
||||
};
|
||||
for (int i = 0; i < 4; i++) {
|
||||
TabLayout.Tab tab = tabLayout.getTabAt(i);
|
||||
tab.setIcon(tabIcons[i]);
|
||||
tab.setContentDescription(pageTitles[i]);
|
||||
}
|
||||
|
||||
if (tabPosition != 0) {
|
||||
TabLayout.Tab tab = tabLayout.getTabAt(tabPosition);
|
||||
if (tab != null) {
|
||||
tab.select();
|
||||
} else {
|
||||
tabPosition = 0;
|
||||
}
|
||||
}
|
||||
|
||||
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab) {
|
||||
viewPager.setCurrentItem(tab.getPosition());
|
||||
|
||||
tintTab(tab, true);
|
||||
|
||||
if (tab.getPosition() == 1) {
|
||||
if (tab.getPosition() == notificationTabPosition) {
|
||||
NotificationHelper.clearNotificationsForActiveAccount(MainActivity.this, accountManager);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabUnselected(TabLayout.Tab tab) {
|
||||
tintTab(tab, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -242,10 +204,6 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
|||
}
|
||||
});
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
tintTab(tabLayout.getTabAt(i), i == tabPosition);
|
||||
}
|
||||
|
||||
// Setup push notifications
|
||||
if (NotificationHelper.areNotificationsEnabled(this, accountManager)) {
|
||||
NotificationHelper.enablePullNotifications();
|
||||
|
@ -260,6 +218,9 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
|||
if (event instanceof ProfileEditedEvent) {
|
||||
onFetchUserInfoSuccess(((ProfileEditedEvent) event).getNewProfileData());
|
||||
}
|
||||
if (event instanceof MainTabsChangedEvent) {
|
||||
setupTabs(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Flush old media that was cached for sharing
|
||||
|
@ -316,9 +277,12 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
|||
}
|
||||
}
|
||||
|
||||
private void tintTab(TabLayout.Tab tab, boolean tinted) {
|
||||
int color = (tinted) ? R.attr.tab_icon_selected_tint : R.attr.toolbar_icon_tint;
|
||||
ThemeUtils.setDrawableTint(this, tab.getIcon(), color);
|
||||
private void forwardShare(Intent intent) {
|
||||
Intent composeIntent = new Intent(this, ComposeActivity.class);
|
||||
composeIntent.setAction(intent.getAction());
|
||||
composeIntent.setType(intent.getType());
|
||||
composeIntent.putExtras(intent);
|
||||
startActivity(composeIntent);
|
||||
}
|
||||
|
||||
private void setupDrawer() {
|
||||
|
@ -433,6 +397,29 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
|||
});
|
||||
}
|
||||
|
||||
private void setupTabs(boolean selectNotificationTab) {
|
||||
List<TabData> tabs = accountManager.getActiveAccount().getTabPreferences();
|
||||
|
||||
MainPagerAdapter adapter = new MainPagerAdapter(tabs, getSupportFragmentManager());
|
||||
viewPager.setAdapter(adapter);
|
||||
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
tabLayout.removeAllTabs();
|
||||
for (int i = 0; i < tabs.size(); i++) {
|
||||
TabLayout.Tab tab = tabLayout.newTab()
|
||||
.setIcon(tabs.get(i).getIcon())
|
||||
.setContentDescription(tabs.get(i).getText());
|
||||
tabLayout.addTab(tab);
|
||||
if(tabs.get(i).getId().equals(TabDataKt.NOTIFICATIONS)) {
|
||||
notificationTabPosition = i;
|
||||
if(selectNotificationTab) {
|
||||
tab.select();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private boolean handleProfileClick(IProfile profile, boolean current) {
|
||||
AccountEntity activeAccount = accountManager.getActiveAccount();
|
||||
|
||||
|
|
|
@ -92,7 +92,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd
|
|||
bar.setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
|
||||
RecyclerView recyclerView = findViewById(R.id.recycler_view);
|
||||
RecyclerView recyclerView = findViewById(R.id.recyclerView);
|
||||
noContent = findViewById(R.id.no_content);
|
||||
recyclerView.setHasFixedSize(true);
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||
|
|
57
app/src/main/java/com/keylesspalace/tusky/TabData.kt
Normal file
57
app/src/main/java/com/keylesspalace/tusky/TabData.kt
Normal file
|
@ -0,0 +1,57 @@
|
|||
/* Copyright 2019 Conny Duck
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.keylesspalace.tusky.components.conversation.ConversationsFragment
|
||||
import com.keylesspalace.tusky.fragment.NotificationsFragment
|
||||
import com.keylesspalace.tusky.fragment.TimelineFragment
|
||||
|
||||
/** this would be a good case for a sealed class, but that does not work nice with Room */
|
||||
|
||||
const val HOME = "Home"
|
||||
const val NOTIFICATIONS = "Notifications"
|
||||
const val LOCAL = "Local"
|
||||
const val FEDERATED = "Federated"
|
||||
const val DIRECT = "Direct"
|
||||
|
||||
data class TabData(val id: String,
|
||||
@StringRes val text: Int,
|
||||
@DrawableRes val icon: Int,
|
||||
val fragment: () -> Fragment)
|
||||
|
||||
|
||||
fun createTabDataFromId(id: String): TabData {
|
||||
return when (id) {
|
||||
HOME -> TabData(HOME, R.string.title_home, R.drawable.ic_home_24dp) { TimelineFragment.newInstance(TimelineFragment.Kind.HOME) }
|
||||
NOTIFICATIONS -> TabData(NOTIFICATIONS, R.string.title_notifications, R.drawable.ic_notifications_24dp) { NotificationsFragment.newInstance() }
|
||||
LOCAL -> TabData(LOCAL, R.string.title_public_local, R.drawable.ic_local_24dp) { TimelineFragment.newInstance(TimelineFragment.Kind.PUBLIC_LOCAL) }
|
||||
FEDERATED -> TabData(FEDERATED, R.string.title_public_federated, R.drawable.ic_public_24dp) { TimelineFragment.newInstance(TimelineFragment.Kind.PUBLIC_FEDERATED) }
|
||||
DIRECT -> TabData(DIRECT, R.string.title_direct_messages, R.drawable.reblog_direct_dark) { ConversationsFragment.newInstance() }
|
||||
else -> throw IllegalArgumentException("unknown tab type")
|
||||
}
|
||||
}
|
||||
|
||||
fun defaultTabs(): List<TabData> {
|
||||
return listOf(
|
||||
createTabDataFromId(HOME),
|
||||
createTabDataFromId(NOTIFICATIONS),
|
||||
createTabDataFromId(LOCAL),
|
||||
createTabDataFromId(FEDERATED)
|
||||
)
|
||||
}
|
|
@ -0,0 +1,205 @@
|
|||
/* Copyright 2019 Conny Duck
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.adapter.ItemInteractionListener
|
||||
import com.keylesspalace.tusky.adapter.TabAdapter
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.MainTabsChangedEvent
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import kotlinx.android.synthetic.main.activity_tab_preference.*
|
||||
import kotlinx.android.synthetic.main.toolbar_basic.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListener {
|
||||
|
||||
@Inject
|
||||
lateinit var eventHub: EventHub
|
||||
|
||||
private lateinit var currentTabs: MutableList<TabData>
|
||||
private lateinit var currentTabsAdapter: TabAdapter
|
||||
private lateinit var touchHelper: ItemTouchHelper
|
||||
private lateinit var addTabAdapter: TabAdapter
|
||||
|
||||
private val selectedItemElevation by lazy { resources.getDimension(R.dimen.selected_drag_item_elevation) }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.activity_tab_preference)
|
||||
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
supportActionBar?.apply {
|
||||
setTitle(R.string.title_tab_preferences)
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setDisplayShowHomeEnabled(true)
|
||||
}
|
||||
|
||||
currentTabs = (accountManager.activeAccount?.tabPreferences ?: emptyList()).toMutableList()
|
||||
currentTabsAdapter = TabAdapter(currentTabs, false, this)
|
||||
currentTabsRecyclerView.adapter = currentTabsAdapter
|
||||
currentTabsRecyclerView.layoutManager = LinearLayoutManager(this)
|
||||
currentTabsRecyclerView.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
|
||||
|
||||
addTabAdapter = TabAdapter(listOf(createTabDataFromId(DIRECT)), true, this)
|
||||
addTabRecyclerView.adapter = addTabAdapter
|
||||
addTabRecyclerView.layoutManager = LinearLayoutManager(this)
|
||||
|
||||
touchHelper = ItemTouchHelper(object: ItemTouchHelper.Callback(){
|
||||
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
||||
return makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.END)
|
||||
}
|
||||
|
||||
override fun isLongPressDragEnabled(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun isItemViewSwipeEnabled(): Boolean {
|
||||
return MIN_TAB_COUNT < currentTabs.size
|
||||
}
|
||||
|
||||
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
|
||||
val temp = currentTabs[viewHolder.adapterPosition]
|
||||
currentTabs[viewHolder.adapterPosition] = currentTabs[target.adapterPosition]
|
||||
currentTabs[target.adapterPosition] = temp
|
||||
|
||||
currentTabsAdapter.notifyItemMoved(viewHolder.adapterPosition, target.adapterPosition)
|
||||
saveTabs()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
currentTabs.removeAt(viewHolder.adapterPosition)
|
||||
currentTabsAdapter.notifyItemRemoved(viewHolder.adapterPosition)
|
||||
updateAvailableTabs()
|
||||
saveTabs()
|
||||
}
|
||||
|
||||
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
|
||||
if(actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
|
||||
viewHolder?.itemView?.elevation = selectedItemElevation
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
|
||||
super.clearView(recyclerView, viewHolder)
|
||||
viewHolder.itemView.elevation = 0f
|
||||
}
|
||||
})
|
||||
|
||||
touchHelper.attachToRecyclerView(currentTabsRecyclerView)
|
||||
|
||||
|
||||
actionButton.setOnClickListener {
|
||||
actionButton.isExpanded = true
|
||||
}
|
||||
|
||||
scrim.setOnClickListener {
|
||||
actionButton.isExpanded = false
|
||||
}
|
||||
|
||||
updateAvailableTabs()
|
||||
|
||||
}
|
||||
|
||||
override fun onTabAdded(tab: TabData) {
|
||||
currentTabs.add(tab)
|
||||
currentTabsAdapter.notifyItemInserted(currentTabs.size - 1)
|
||||
actionButton.isExpanded = false
|
||||
updateAvailableTabs()
|
||||
saveTabs()
|
||||
}
|
||||
|
||||
private fun updateAvailableTabs() {
|
||||
val addableTabs: MutableList<TabData> = mutableListOf()
|
||||
|
||||
val homeTab = createTabDataFromId(HOME)
|
||||
if(!currentTabs.contains(homeTab)) {
|
||||
addableTabs.add(homeTab)
|
||||
}
|
||||
val notificationTab = createTabDataFromId(NOTIFICATIONS)
|
||||
if(!currentTabs.contains(notificationTab)) {
|
||||
addableTabs.add(notificationTab)
|
||||
}
|
||||
val localTab = createTabDataFromId(LOCAL)
|
||||
if(!currentTabs.contains(localTab)) {
|
||||
addableTabs.add(localTab)
|
||||
}
|
||||
val federatedTab = createTabDataFromId(FEDERATED)
|
||||
if(!currentTabs.contains(federatedTab)) {
|
||||
addableTabs.add(federatedTab)
|
||||
}
|
||||
val directMessagesTab = createTabDataFromId(DIRECT)
|
||||
if(!currentTabs.contains(directMessagesTab)) {
|
||||
addableTabs.add(directMessagesTab)
|
||||
}
|
||||
|
||||
addTabAdapter.updateData(addableTabs)
|
||||
|
||||
maxTabsInfo.visible(addableTabs.size == 0 || currentTabs.size >= MAX_TAB_COUNT)
|
||||
|
||||
}
|
||||
|
||||
override fun onStartDelete(viewHolder: RecyclerView.ViewHolder) {
|
||||
touchHelper.startSwipe(viewHolder)
|
||||
}
|
||||
|
||||
override fun onStartDrag(viewHolder: RecyclerView.ViewHolder) {
|
||||
touchHelper.startDrag(viewHolder)
|
||||
}
|
||||
|
||||
private fun saveTabs() {
|
||||
accountManager.activeAccount?.let {
|
||||
it.tabPreferences = currentTabs
|
||||
accountManager.saveAccount(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (actionButton.isExpanded) {
|
||||
actionButton.isExpanded = false
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
eventHub.dispatch(MainTabsChangedEvent(currentTabs))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MIN_TAB_COUNT = 2
|
||||
private const val MAX_TAB_COUNT = 5
|
||||
}
|
||||
|
||||
}
|
|
@ -66,7 +66,8 @@ public class TuskyApplication extends Application implements HasActivityInjector
|
|||
.allowMainThreadQueries()
|
||||
.addMigrations(AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4, AppDatabase.MIGRATION_4_5,
|
||||
AppDatabase.MIGRATION_5_6, AppDatabase.MIGRATION_6_7, AppDatabase.MIGRATION_7_8,
|
||||
AppDatabase.MIGRATION_8_9, AppDatabase.MIGRATION_9_10, AppDatabase.MIGRATION_10_11)
|
||||
AppDatabase.MIGRATION_8_9, AppDatabase.MIGRATION_9_10, AppDatabase.MIGRATION_10_11,
|
||||
AppDatabase.MIGRATION_11_12)
|
||||
.build();
|
||||
accountManager = new AccountManager(appDatabase);
|
||||
serviceLocator = new ServiceLocator() {
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/* Copyright 2019 Conny Duck
|
||||
*
|
||||
* 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.adapter
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
import com.keylesspalace.tusky.util.Status
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import kotlinx.android.synthetic.main.item_network_state.view.*
|
||||
|
||||
class NetworkStateViewHolder(itemView: View,
|
||||
private val retryCallback: () -> Unit)
|
||||
: RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
fun setUpWithNetworkState(state: NetworkState?, fullScreen: Boolean) {
|
||||
itemView.progressBar.visible(state?.status == Status.RUNNING)
|
||||
itemView.retryButton.visible(state?.status == Status.FAILED)
|
||||
itemView.errorMsg.visible(state?.msg != null)
|
||||
itemView.errorMsg.text = state?.msg
|
||||
itemView.retryButton.setOnClickListener {
|
||||
retryCallback()
|
||||
}
|
||||
if(fullScreen) {
|
||||
itemView.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
} else {
|
||||
itemView.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -31,7 +31,7 @@ public final class PlaceholderViewHolder extends RecyclerView.ViewHolder {
|
|||
PlaceholderViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
loadMoreButton = itemView.findViewById(R.id.button_load_more);
|
||||
progressBar = itemView.findViewById(R.id.progress_bar);
|
||||
progressBar = itemView.findViewById(R.id.progressBar);
|
||||
}
|
||||
|
||||
public void setup(final StatusActionListener listener, boolean progress) {
|
||||
|
|
|
@ -44,7 +44,7 @@ import java.lang.CharSequence;
|
|||
import at.connyduck.sparkbutton.SparkButton;
|
||||
import at.connyduck.sparkbutton.SparkEventListener;
|
||||
|
||||
abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||
public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private TextView displayName;
|
||||
private TextView username;
|
||||
|
@ -54,23 +54,23 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
private ImageButton moreButton;
|
||||
private boolean favourited;
|
||||
private boolean reblogged;
|
||||
private MediaPreviewImageView[] mediaPreviews;
|
||||
protected MediaPreviewImageView[] mediaPreviews;
|
||||
private ImageView[] mediaOverlays;
|
||||
private TextView sensitiveMediaWarning;
|
||||
private View sensitiveMediaShow;
|
||||
private TextView mediaLabel;
|
||||
protected TextView mediaLabel;
|
||||
private ToggleButton contentWarningButton;
|
||||
|
||||
ImageView avatar;
|
||||
TextView timestampInfo;
|
||||
TextView content;
|
||||
TextView contentWarningDescription;
|
||||
public ImageView avatar;
|
||||
public TextView timestampInfo;
|
||||
public TextView content;
|
||||
public TextView contentWarningDescription;
|
||||
|
||||
private boolean useAbsoluteTime;
|
||||
private SimpleDateFormat shortSdf;
|
||||
private SimpleDateFormat longSdf;
|
||||
|
||||
StatusBaseViewHolder(View itemView, boolean useAbsoluteTime) {
|
||||
protected StatusBaseViewHolder(View itemView, boolean useAbsoluteTime) {
|
||||
super(itemView);
|
||||
displayName = itemView.findViewById(R.id.status_display_name);
|
||||
username = itemView.findViewById(R.id.status_username);
|
||||
|
@ -108,28 +108,30 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
|
||||
protected abstract int getMediaPreviewHeight(Context context);
|
||||
|
||||
private void setDisplayName(String name, List<Emoji> customEmojis) {
|
||||
protected void setDisplayName(String name, List<Emoji> customEmojis) {
|
||||
CharSequence emojifiedName = CustomEmojiHelper.emojifyString(name, customEmojis, displayName);
|
||||
displayName.setText(emojifiedName);
|
||||
}
|
||||
|
||||
private void setUsername(String name) {
|
||||
protected void setUsername(String name) {
|
||||
Context context = username.getContext();
|
||||
String format = context.getString(R.string.status_username_format);
|
||||
String usernameText = String.format(format, name);
|
||||
username.setText(usernameText);
|
||||
}
|
||||
|
||||
private void setSpoilerAndContent(StatusViewData.Concrete status,
|
||||
final StatusActionListener listener) {
|
||||
if (status.getSpoilerText() == null || status.getSpoilerText().isEmpty()) {
|
||||
protected void setSpoilerAndContent(boolean expanded,
|
||||
@NonNull Spanned content,
|
||||
@Nullable String spoilerText,
|
||||
@Nullable Status.Mention[] mentions,
|
||||
@NonNull List<Emoji> emojis,
|
||||
final StatusActionListener listener) {
|
||||
if (TextUtils.isEmpty(spoilerText)) {
|
||||
contentWarningDescription.setVisibility(View.GONE);
|
||||
contentWarningButton.setVisibility(View.GONE);
|
||||
this.setTextVisible(true, status, listener);
|
||||
this.setTextVisible(true, content, mentions, emojis, listener);
|
||||
} else {
|
||||
boolean expanded = status.isExpanded();
|
||||
CharSequence emojiSpoiler = CustomEmojiHelper.emojifyString(
|
||||
status.getSpoilerText(), status.getStatusEmojis(), contentWarningDescription);
|
||||
CharSequence emojiSpoiler = CustomEmojiHelper.emojifyString(spoilerText, emojis, contentWarningDescription);
|
||||
contentWarningDescription.setText(emojiSpoiler);
|
||||
contentWarningDescription.setVisibility(View.VISIBLE);
|
||||
contentWarningButton.setVisibility(View.VISIBLE);
|
||||
|
@ -139,18 +141,19 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
|
||||
listener.onExpandedChange(isChecked, getAdapterPosition());
|
||||
}
|
||||
this.setTextVisible(isChecked, status, listener);
|
||||
this.setTextVisible(isChecked, content, mentions, emojis, listener);
|
||||
});
|
||||
this.setTextVisible(expanded, status, listener);
|
||||
this.setTextVisible(expanded, content, mentions, emojis, listener);
|
||||
}
|
||||
}
|
||||
|
||||
private void setTextVisible(boolean expanded, StatusViewData.Concrete status,
|
||||
private void setTextVisible(boolean expanded,
|
||||
Spanned content,
|
||||
Status.Mention[] mentions,
|
||||
List<Emoji> emojis,
|
||||
final StatusActionListener listener) {
|
||||
Status.Mention[] mentions = status.getMentions();
|
||||
if (expanded) {
|
||||
Spanned emojifiedText = CustomEmojiHelper.emojifyText(
|
||||
status.getContent(), status.getStatusEmojis(), this.content);
|
||||
Spanned emojifiedText = CustomEmojiHelper.emojifyText(content, emojis, this.content);
|
||||
LinkHelper.setClickableText(this.content, emojifiedText, mentions, listener);
|
||||
} else {
|
||||
LinkHelper.setClickableMentions(this.content, mentions, listener);
|
||||
|
@ -162,7 +165,7 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
}
|
||||
|
||||
void setAvatar(String url, @Nullable String rebloggedUrl) {
|
||||
protected void setAvatar(String url, @Nullable String rebloggedUrl) {
|
||||
if (TextUtils.isEmpty(url)) {
|
||||
avatar.setImageResource(R.drawable.avatar_default);
|
||||
} else {
|
||||
|
@ -219,7 +222,7 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
}
|
||||
|
||||
private void setIsReply(boolean isReply) {
|
||||
protected void setIsReply(boolean isReply) {
|
||||
if (isReply) {
|
||||
replyButton.setImageResource(R.drawable.ic_reply_all_24dp);
|
||||
} else {
|
||||
|
@ -265,13 +268,13 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
}
|
||||
|
||||
private void setFavourited(boolean favourited) {
|
||||
protected void setFavourited(boolean favourited) {
|
||||
this.favourited = favourited;
|
||||
favouriteButton.setChecked(favourited);
|
||||
}
|
||||
|
||||
private void setMediaPreviews(final List<Attachment> attachments, boolean sensitive,
|
||||
final StatusActionListener listener, boolean showingContent) {
|
||||
protected void setMediaPreviews(final List<Attachment> attachments, boolean sensitive,
|
||||
final StatusActionListener listener, boolean showingContent) {
|
||||
|
||||
Context context = itemView.getContext();
|
||||
|
||||
|
@ -406,8 +409,8 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
}
|
||||
|
||||
private void setMediaLabel(List<Attachment> attachments, boolean sensitive,
|
||||
final StatusActionListener listener) {
|
||||
protected void setMediaLabel(List<Attachment> attachments, boolean sensitive,
|
||||
final StatusActionListener listener) {
|
||||
if (attachments.size() == 0) {
|
||||
mediaLabel.setVisibility(View.GONE);
|
||||
return;
|
||||
|
@ -432,12 +435,12 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
mediaLabel.setOnClickListener(v -> listener.onViewMedia(getAdapterPosition(), 0, null));
|
||||
}
|
||||
|
||||
private void hideSensitiveMediaWarning() {
|
||||
protected void hideSensitiveMediaWarning() {
|
||||
sensitiveMediaWarning.setVisibility(View.GONE);
|
||||
sensitiveMediaShow.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void setupButtons(final StatusActionListener listener, final String accountId) {
|
||||
protected void setupButtons(final StatusActionListener listener, final String accountId) {
|
||||
/* Originally position was passed through to all these listeners, but it caused several
|
||||
* bugs where other statuses in the list would be removed or added and cause the position
|
||||
* here to become outdated. So, getting the adapter position at the time the listener is
|
||||
|
@ -449,23 +452,25 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
listener.onReply(position);
|
||||
}
|
||||
});
|
||||
reblogButton.setEventListener(new SparkEventListener() {
|
||||
@Override
|
||||
public void onEvent(ImageView button, boolean buttonState) {
|
||||
int position = getAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
listener.onReblog(!reblogged, position);
|
||||
if(reblogButton != null) {
|
||||
reblogButton.setEventListener(new SparkEventListener() {
|
||||
@Override
|
||||
public void onEvent(ImageView button, boolean buttonState) {
|
||||
int position = getAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
listener.onReblog(!reblogged, position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEventAnimationEnd(ImageView button, boolean buttonState) {
|
||||
}
|
||||
@Override
|
||||
public void onEventAnimationEnd(ImageView button, boolean buttonState) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEventAnimationStart(ImageView button, boolean buttonState) {
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public void onEventAnimationStart(ImageView button, boolean buttonState) {
|
||||
}
|
||||
});
|
||||
}
|
||||
favouriteButton.setEventListener(new SparkEventListener() {
|
||||
@Override
|
||||
public void onEvent(ImageView button, boolean buttonState) {
|
||||
|
@ -503,8 +508,8 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
itemView.setOnClickListener(viewThreadListener);
|
||||
}
|
||||
|
||||
void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
|
||||
boolean mediaPreviewEnabled) {
|
||||
protected void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
|
||||
boolean mediaPreviewEnabled) {
|
||||
setDisplayName(status.getUserFullName(), status.getAccountEmojis());
|
||||
setUsername(status.getNickname());
|
||||
setCreatedAt(status.getCreatedAt());
|
||||
|
@ -535,7 +540,7 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
setupButtons(listener, status.getSenderId());
|
||||
setRebloggingEnabled(status.getRebloggingEnabled(), status.getVisibility());
|
||||
|
||||
setSpoilerAndContent(status, listener);
|
||||
setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getStatusEmojis(), listener);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -130,8 +130,8 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
|||
}
|
||||
|
||||
@Override
|
||||
void setupWithStatus(final StatusViewData.Concrete status, final StatusActionListener listener,
|
||||
boolean mediaPreviewEnabled) {
|
||||
protected void setupWithStatus(final StatusViewData.Concrete status, final StatusActionListener listener,
|
||||
boolean mediaPreviewEnabled) {
|
||||
super.setupWithStatus(status, listener, mediaPreviewEnabled);
|
||||
|
||||
setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener);
|
||||
|
|
|
@ -49,7 +49,7 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
|||
}
|
||||
|
||||
@Override
|
||||
void setAvatar(String url, @Nullable String rebloggedUrl) {
|
||||
protected void setAvatar(String url, @Nullable String rebloggedUrl) {
|
||||
super.setAvatar(url, rebloggedUrl);
|
||||
|
||||
Context context = avatar.getContext();
|
||||
|
@ -75,8 +75,8 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
|||
}
|
||||
|
||||
@Override
|
||||
void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
|
||||
boolean mediaPreviewEnabled) {
|
||||
protected void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
|
||||
boolean mediaPreviewEnabled) {
|
||||
if(status == null) {
|
||||
showContent(false);
|
||||
} else {
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/* Copyright 2019 Conny Duck
|
||||
*
|
||||
* 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.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.TabData
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import kotlinx.android.synthetic.main.item_tab_preference.view.*
|
||||
|
||||
interface ItemInteractionListener {
|
||||
fun onTabAdded(tab: TabData)
|
||||
fun onStartDelete(viewHolder: RecyclerView.ViewHolder)
|
||||
fun onStartDrag(viewHolder: RecyclerView.ViewHolder)
|
||||
}
|
||||
|
||||
class TabAdapter(var data: List<TabData>,
|
||||
val small: Boolean = false,
|
||||
val listener: ItemInteractionListener? = null) : RecyclerView.Adapter<TabAdapter.ViewHolder>() {
|
||||
|
||||
fun updateData(newData: List<TabData>) {
|
||||
this.data = newData
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val layoutId = if(small) {
|
||||
R.layout.item_tab_preference_small
|
||||
} else {
|
||||
R.layout.item_tab_preference
|
||||
}
|
||||
val view = LayoutInflater.from(parent.context).inflate(layoutId, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.itemView.textView.setText(data[position].text)
|
||||
val iconDrawable = ThemeUtils.getTintedDrawable(holder.itemView.context, data[position].icon, android.R.attr.textColorSecondary)
|
||||
holder.itemView.textView.setCompoundDrawablesRelativeWithIntrinsicBounds(iconDrawable, null, null, null)
|
||||
if(small) {
|
||||
holder.itemView.textView.setOnClickListener {
|
||||
listener?.onTabAdded(data[position])
|
||||
}
|
||||
}
|
||||
holder.itemView.imageView?.setOnTouchListener { _, event ->
|
||||
if(event.action == MotionEvent.ACTION_DOWN) {
|
||||
listener?.onStartDrag(holder)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return data.size
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
||||
}
|
|
@ -10,7 +10,7 @@ import javax.inject.Inject
|
|||
class CacheUpdater @Inject constructor(
|
||||
eventHub: EventHub,
|
||||
accountManager: AccountManager,
|
||||
val appDatabase: AppDatabase
|
||||
private val appDatabase: AppDatabase
|
||||
) {
|
||||
|
||||
private val disposable: Disposable
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.keylesspalace.tusky.appstore
|
||||
|
||||
import com.keylesspalace.tusky.TabData
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
|
||||
|
@ -11,4 +12,5 @@ data class MuteEvent(val accountId: String) : Dispatchable
|
|||
data class StatusDeletedEvent(val statusId: String) : Dispatchable
|
||||
data class StatusComposedEvent(val status: Status) : Dispatchable
|
||||
data class ProfileEditedEvent(val newProfileData: Account) : Dispatchable
|
||||
data class PreferenceChangedEvent(val preferenceKey: String) : Dispatchable
|
||||
data class PreferenceChangedEvent(val preferenceKey: String) : Dispatchable
|
||||
data class MainTabsChangedEvent(val newTabs: List<TabData>) : Dispatchable
|
|
@ -0,0 +1,108 @@
|
|||
package com.keylesspalace.tusky.components.conversation
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.paging.AsyncPagedListDiffer
|
||||
import androidx.paging.PagedList
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListUpdateCallback
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.adapter.NetworkStateViewHolder
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
|
||||
class ConversationAdapter(private val useAbsoluteTime: Boolean,
|
||||
private val mediaPreviewEnabled: Boolean,
|
||||
private val listener: StatusActionListener,
|
||||
private val topLoadedCallback: () -> Unit,
|
||||
private val retryCallback: () -> Unit)
|
||||
: RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
private var networkState: NetworkState? = null
|
||||
|
||||
private val differ: AsyncPagedListDiffer<ConversationEntity> = AsyncPagedListDiffer(object: ListUpdateCallback {
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
notifyItemRangeInserted(position, count)
|
||||
if(position == 0) {
|
||||
topLoadedCallback()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRemoved(position: Int, count: Int) {
|
||||
notifyItemRangeRemoved(position, count)
|
||||
}
|
||||
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||
notifyItemMoved(fromPosition, toPosition)
|
||||
}
|
||||
|
||||
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||
notifyItemRangeChanged(position, count, payload)
|
||||
}
|
||||
}, AsyncDifferConfig.Builder<ConversationEntity>(CONVERSATION_COMPARATOR).build())
|
||||
|
||||
fun submitList(list: PagedList<ConversationEntity>) {
|
||||
differ.submitList(list)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
|
||||
return when (viewType) {
|
||||
R.layout.item_network_state -> NetworkStateViewHolder(view, retryCallback)
|
||||
R.layout.item_conversation -> ConversationViewHolder(view, listener, useAbsoluteTime, mediaPreviewEnabled)
|
||||
else -> throw IllegalArgumentException("unknown view type $viewType")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (getItemViewType(position)) {
|
||||
R.layout.item_network_state -> (holder as NetworkStateViewHolder).setUpWithNetworkState(networkState, differ.itemCount == 0)
|
||||
R.layout.item_conversation -> (holder as ConversationViewHolder).setupWithConversation(differ.getItem(position))
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasExtraRow() = networkState != null && networkState != NetworkState.LOADED
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (hasExtraRow() && position == itemCount - 1) {
|
||||
R.layout.item_network_state
|
||||
} else {
|
||||
R.layout.item_conversation
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return differ.itemCount + if (hasExtraRow()) 1 else 0
|
||||
}
|
||||
|
||||
fun setNetworkState(newNetworkState: NetworkState?) {
|
||||
val previousState = this.networkState
|
||||
val hadExtraRow = hasExtraRow()
|
||||
this.networkState = newNetworkState
|
||||
val hasExtraRow = hasExtraRow()
|
||||
if (hadExtraRow != hasExtraRow) {
|
||||
if (hadExtraRow) {
|
||||
notifyItemRemoved(differ.itemCount)
|
||||
} else {
|
||||
notifyItemInserted(differ.itemCount)
|
||||
}
|
||||
} else if (hasExtraRow && previousState != newNetworkState) {
|
||||
notifyItemChanged(itemCount - 1)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback<ConversationEntity>() {
|
||||
override fun areContentsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean =
|
||||
oldItem == newItem
|
||||
|
||||
override fun areItemsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean =
|
||||
oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
/* Copyright 2019 Conny Duck
|
||||
*
|
||||
* 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.conversation
|
||||
|
||||
import android.text.Spanned
|
||||
import android.text.SpannedString
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.TypeConverters
|
||||
import com.keylesspalace.tusky.db.Converters
|
||||
import com.keylesspalace.tusky.entity.*
|
||||
import com.keylesspalace.tusky.util.SmartLengthInputFilter
|
||||
import java.util.*
|
||||
|
||||
@Entity(primaryKeys = ["id","accountId"])
|
||||
@TypeConverters(Converters::class)
|
||||
data class ConversationEntity(
|
||||
val accountId: Long,
|
||||
val id: String,
|
||||
val accounts: List<ConversationAccountEntity>,
|
||||
val unread: Boolean,
|
||||
@Embedded(prefix = "s_") val lastStatus: ConversationStatusEntity
|
||||
)
|
||||
|
||||
data class ConversationAccountEntity(
|
||||
val id: String,
|
||||
val username: String,
|
||||
val displayName: String,
|
||||
val avatar: String,
|
||||
val emojis: List<Emoji>
|
||||
) {
|
||||
fun toAccount(): Account {
|
||||
return Account(
|
||||
id = id,
|
||||
username = username,
|
||||
displayName = displayName,
|
||||
avatar = avatar,
|
||||
emojis = emojis,
|
||||
url = "",
|
||||
localUsername = "",
|
||||
note = SpannedString(""),
|
||||
header = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@TypeConverters(Converters::class)
|
||||
data class ConversationStatusEntity(
|
||||
val id: String,
|
||||
val url: String?,
|
||||
val inReplyToId: String?,
|
||||
val inReplyToAccountId: String?,
|
||||
val account: ConversationAccountEntity,
|
||||
val content: Spanned,
|
||||
val createdAt: Date,
|
||||
val emojis: List<Emoji>,
|
||||
val favouritesCount: Int,
|
||||
val favourited: Boolean,
|
||||
val sensitive: Boolean,
|
||||
val spoilerText: String,
|
||||
val attachments: List<Attachment>,
|
||||
val mentions: Array<Status.Mention>,
|
||||
val showingHiddenContent: Boolean,
|
||||
val expanded: Boolean,
|
||||
val collapsible: Boolean,
|
||||
val collapsed: Boolean
|
||||
|
||||
) {
|
||||
/** its necessary to override this because Spanned.equals does not work as expected */
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as ConversationStatusEntity
|
||||
|
||||
if (id != other.id) return false
|
||||
if (url != other.url) return false
|
||||
if (inReplyToId != other.inReplyToId) return false
|
||||
if (inReplyToAccountId != other.inReplyToAccountId) return false
|
||||
if (account != other.account) return false
|
||||
if (content.toString() != other.content.toString()) return false //TODO find a better method to compare two spanned strings
|
||||
if (createdAt != other.createdAt) return false
|
||||
if (emojis != other.emojis) return false
|
||||
if (favouritesCount != other.favouritesCount) return false
|
||||
if (favourited != other.favourited) return false
|
||||
if (sensitive != other.sensitive) return false
|
||||
if (spoilerText != other.spoilerText) return false
|
||||
if (attachments != other.attachments) return false
|
||||
if (!mentions.contentEquals(other.mentions)) return false
|
||||
if (showingHiddenContent != other.showingHiddenContent) return false
|
||||
if (expanded != other.expanded) return false
|
||||
if (collapsible != other.collapsible) return false
|
||||
if (collapsed != other.collapsed) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + (url?.hashCode() ?: 0)
|
||||
result = 31 * result + (inReplyToId?.hashCode() ?: 0)
|
||||
result = 31 * result + (inReplyToAccountId?.hashCode() ?: 0)
|
||||
result = 31 * result + account.hashCode()
|
||||
result = 31 * result + content.hashCode()
|
||||
result = 31 * result + createdAt.hashCode()
|
||||
result = 31 * result + emojis.hashCode()
|
||||
result = 31 * result + favouritesCount
|
||||
result = 31 * result + favourited.hashCode()
|
||||
result = 31 * result + sensitive.hashCode()
|
||||
result = 31 * result + spoilerText.hashCode()
|
||||
result = 31 * result + attachments.hashCode()
|
||||
result = 31 * result + mentions.contentHashCode()
|
||||
result = 31 * result + showingHiddenContent.hashCode()
|
||||
result = 31 * result + expanded.hashCode()
|
||||
result = 31 * result + collapsible.hashCode()
|
||||
result = 31 * result + collapsed.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
fun toStatus(): Status {
|
||||
return Status(
|
||||
id = id,
|
||||
url = url,
|
||||
account = account.toAccount(),
|
||||
inReplyToId = inReplyToId,
|
||||
inReplyToAccountId = inReplyToAccountId,
|
||||
content = content,
|
||||
reblog = null,
|
||||
createdAt = createdAt,
|
||||
emojis = emojis,
|
||||
reblogsCount = 0,
|
||||
favouritesCount = favouritesCount,
|
||||
reblogged = false,
|
||||
favourited = favourited,
|
||||
sensitive= sensitive,
|
||||
spoilerText = spoilerText,
|
||||
visibility = Status.Visibility.PRIVATE,
|
||||
attachments = attachments,
|
||||
mentions = mentions,
|
||||
application = null,
|
||||
pinned = false)
|
||||
}
|
||||
}
|
||||
|
||||
fun Account.toEntity() =
|
||||
ConversationAccountEntity(
|
||||
id,
|
||||
username,
|
||||
displayName,
|
||||
avatar,
|
||||
emojis ?: emptyList()
|
||||
)
|
||||
|
||||
fun Status.toEntity() =
|
||||
ConversationStatusEntity(
|
||||
id, url, inReplyToId, inReplyToAccountId, account.toEntity(), content,
|
||||
createdAt, emojis, favouritesCount, favourited, sensitive,
|
||||
spoilerText, attachments, mentions,
|
||||
false,
|
||||
false,
|
||||
!SmartLengthInputFilter.hasBadRatio(content, SmartLengthInputFilter.LENGTH_DEFAULT),
|
||||
true
|
||||
)
|
||||
|
||||
|
||||
fun Conversation.toEntity(accountId: Long) =
|
||||
ConversationEntity(
|
||||
accountId,
|
||||
id,
|
||||
accounts.map { it.toEntity() },
|
||||
unread,
|
||||
lastStatus.toEntity()
|
||||
)
|
|
@ -0,0 +1,157 @@
|
|||
/* 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.components.conversation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.InputFilter;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.ToggleButton;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder;
|
||||
import com.keylesspalace.tusky.entity.Attachment;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
import com.keylesspalace.tusky.util.SmartLengthInputFilter;
|
||||
import com.squareup.picasso.Picasso;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class ConversationViewHolder extends StatusBaseViewHolder {
|
||||
private static final InputFilter[] COLLAPSE_INPUT_FILTER = new InputFilter[]{SmartLengthInputFilter.INSTANCE};
|
||||
private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0];
|
||||
|
||||
private TextView conversationNameTextView;
|
||||
private ToggleButton contentCollapseButton;
|
||||
private ImageView[] avatars;
|
||||
|
||||
private StatusActionListener listener;
|
||||
private boolean mediaPreviewEnabled;
|
||||
|
||||
ConversationViewHolder(View itemView,
|
||||
StatusActionListener listener,
|
||||
boolean useAbsoluteTime,
|
||||
boolean mediaPreviewEnabled) {
|
||||
super(itemView, useAbsoluteTime);
|
||||
conversationNameTextView = itemView.findViewById(R.id.conversation_name);
|
||||
contentCollapseButton = itemView.findViewById(R.id.button_toggle_content);
|
||||
avatars = new ImageView[]{avatar, itemView.findViewById(R.id.status_avatar_1), itemView.findViewById(R.id.status_avatar_2)};
|
||||
|
||||
this.listener = listener;
|
||||
this.mediaPreviewEnabled = mediaPreviewEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getMediaPreviewHeight(Context context) {
|
||||
return context.getResources().getDimensionPixelSize(R.dimen.status_media_preview_height);
|
||||
}
|
||||
|
||||
void setupWithConversation(ConversationEntity conversation) {
|
||||
ConversationStatusEntity status = conversation.getLastStatus();
|
||||
ConversationAccountEntity account = status.getAccount();
|
||||
|
||||
setupCollapsedState(status.getCollapsible(), status.getCollapsed(), status.getExpanded(), status.getSpoilerText(), listener);
|
||||
|
||||
setDisplayName(account.getDisplayName(), account.getEmojis());
|
||||
setUsername(account.getUsername());
|
||||
setCreatedAt(status.getCreatedAt());
|
||||
setIsReply(status.getInReplyToId() != null);
|
||||
setFavourited(status.getFavourited());
|
||||
List<Attachment> attachments = status.getAttachments();
|
||||
boolean sensitive = status.getSensitive();
|
||||
if(mediaPreviewEnabled) {
|
||||
setMediaPreviews(attachments, sensitive, listener, status.getShowingHiddenContent());
|
||||
|
||||
if (attachments.size() == 0) {
|
||||
hideSensitiveMediaWarning();
|
||||
}
|
||||
// Hide the unused label.
|
||||
mediaLabel.setVisibility(View.GONE);
|
||||
} else {
|
||||
setMediaLabel(attachments, sensitive, listener);
|
||||
// Hide all unused views.
|
||||
mediaPreviews[0].setVisibility(View.GONE);
|
||||
mediaPreviews[1].setVisibility(View.GONE);
|
||||
mediaPreviews[2].setVisibility(View.GONE);
|
||||
mediaPreviews[3].setVisibility(View.GONE);
|
||||
hideSensitiveMediaWarning();
|
||||
}
|
||||
|
||||
setupButtons(listener, account.getId());
|
||||
|
||||
setSpoilerAndContent(status.getExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getEmojis(), listener);
|
||||
|
||||
setConversationName(conversation.getAccounts());
|
||||
|
||||
setAvatars(conversation.getAccounts());
|
||||
|
||||
}
|
||||
|
||||
private void setConversationName(List<ConversationAccountEntity> accounts) {
|
||||
Context context = conversationNameTextView.getContext();
|
||||
String conversationName;
|
||||
if(accounts.size() == 1) {
|
||||
conversationName = context.getString(R.string.conversation_1_recipients, accounts.get(0).getUsername());
|
||||
} else if(accounts.size() == 2) {
|
||||
conversationName = context.getString(R.string.conversation_2_recipients, accounts.get(0).getUsername(), accounts.get(1).getUsername());
|
||||
} else {
|
||||
conversationName = context.getString(R.string.conversation_more_recipients, accounts.get(0).getUsername(), accounts.get(1).getUsername(), accounts.size() - 2);
|
||||
}
|
||||
|
||||
conversationNameTextView.setText(conversationName);
|
||||
}
|
||||
|
||||
private void setAvatars(List<ConversationAccountEntity> accounts) {
|
||||
for(int i=0; i < avatars.length; i++) {
|
||||
ImageView avatarView = avatars[i];
|
||||
if(i < accounts.size()) {
|
||||
Picasso.with(avatarView.getContext())
|
||||
.load(accounts.get(i).getAvatar())
|
||||
.into(avatarView);
|
||||
avatarView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
avatarView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setupCollapsedState(boolean collapsible, boolean collapsed, boolean expanded, String spoilerText, final StatusActionListener listener) {
|
||||
/* input filter for TextViews have to be set before text */
|
||||
if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) {
|
||||
contentCollapseButton.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
int position = getAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION)
|
||||
listener.onContentCollapsedChange(isChecked, position);
|
||||
});
|
||||
|
||||
contentCollapseButton.setVisibility(View.VISIBLE);
|
||||
if (collapsed) {
|
||||
contentCollapseButton.setChecked(true);
|
||||
content.setFilters(COLLAPSE_INPUT_FILTER);
|
||||
} else {
|
||||
contentCollapseButton.setChecked(false);
|
||||
content.setFilters(NO_INPUT_FILTER);
|
||||
}
|
||||
} else {
|
||||
contentCollapseButton.setVisibility(View.GONE);
|
||||
content.setFilters(NO_INPUT_FILTER);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.keylesspalace.tusky.components.conversation
|
||||
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.paging.PagedList
|
||||
import com.keylesspalace.tusky.entity.Conversation
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.PagingRequestHelper
|
||||
import com.keylesspalace.tusky.util.createStatusLiveData
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
/**
|
||||
* This boundary callback gets notified when user reaches to the edges of the list such that the
|
||||
* database cannot provide any more data.
|
||||
* <p>
|
||||
* The boundary callback might be called multiple times for the same direction so it does its own
|
||||
* rate limiting using the PagingRequestHelper class.
|
||||
*/
|
||||
class ConversationsBoundaryCallback(
|
||||
private val accountId: Long,
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val handleResponse: (Long, List<Conversation>?) -> Unit,
|
||||
private val ioExecutor: Executor,
|
||||
private val networkPageSize: Int)
|
||||
: PagedList.BoundaryCallback<ConversationEntity>() {
|
||||
|
||||
val helper = PagingRequestHelper(ioExecutor)
|
||||
val networkState = helper.createStatusLiveData()
|
||||
|
||||
/**
|
||||
* Database returned 0 items. We should query the backend for more items.
|
||||
*/
|
||||
@MainThread
|
||||
override fun onZeroItemsLoaded() {
|
||||
helper.runIfNotRunning(PagingRequestHelper.RequestType.INITIAL) {
|
||||
mastodonApi.getConversations(null, networkPageSize)
|
||||
.enqueue(createWebserviceCallback(it))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* User reached to the end of the list.
|
||||
*/
|
||||
@MainThread
|
||||
override fun onItemAtEndLoaded(itemAtEnd: ConversationEntity) {
|
||||
helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) {
|
||||
mastodonApi.getConversations(itemAtEnd.lastStatus.id, networkPageSize)
|
||||
.enqueue(createWebserviceCallback(it))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* every time it gets new items, boundary callback simply inserts them into the database and
|
||||
* paging library takes care of refreshing the list if necessary.
|
||||
*/
|
||||
private fun insertItemsIntoDb(
|
||||
response: Response<List<Conversation>>,
|
||||
it: PagingRequestHelper.Request.Callback) {
|
||||
ioExecutor.execute {
|
||||
handleResponse(accountId, response.body())
|
||||
it.recordSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemAtFrontLoaded(itemAtFront: ConversationEntity) {
|
||||
// ignored, since we only ever append to what's in the DB
|
||||
}
|
||||
|
||||
private fun createWebserviceCallback(it: PagingRequestHelper.Request.Callback): Callback<List<Conversation>> {
|
||||
return object : Callback<List<Conversation>> {
|
||||
override fun onFailure(call: Call<List<Conversation>>, t: Throwable) {
|
||||
it.recordFailure(t)
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call<List<Conversation>>, response: Response<List<Conversation>>) {
|
||||
insertItemsIntoDb(response, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
/* Copyright 2019 Conny Duck
|
||||
*
|
||||
* 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.conversation
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import androidx.paging.PagedList
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.keylesspalace.tusky.AccountActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.ViewTagActivity
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.fragment.SFragment
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||
import com.keylesspalace.tusky.network.TimelineCases
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import kotlinx.android.synthetic.main.fragment_timeline.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class ConversationsFragment : SFragment(), StatusActionListener, Injectable {
|
||||
|
||||
@Inject
|
||||
lateinit var timelineCases: TimelineCases
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelFactory
|
||||
@Inject
|
||||
lateinit var db: AppDatabase
|
||||
|
||||
private lateinit var viewModel: ConversationsViewModel
|
||||
|
||||
private lateinit var adapter: ConversationAdapter
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
viewModel = ViewModelProviders.of(this, viewModelFactory)[ConversationsViewModel::class.java]
|
||||
|
||||
return inflater.inflate(R.layout.fragment_timeline, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(view.context)
|
||||
val useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false)
|
||||
|
||||
val account = accountManager.activeAccount
|
||||
val mediaPreviewEnabled = account?.mediaPreviewEnabled ?: true
|
||||
|
||||
|
||||
adapter = ConversationAdapter(useAbsoluteTime, mediaPreviewEnabled,this, ::onTopLoaded, viewModel::retry)
|
||||
|
||||
val divider = DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)
|
||||
val drawable = ThemeUtils.getDrawable(view.context, R.attr.status_divider_drawable, R.drawable.status_divider_dark)
|
||||
divider.setDrawable(drawable)
|
||||
recyclerView.addItemDecoration(divider)
|
||||
recyclerView.layoutManager = LinearLayoutManager(view.context)
|
||||
recyclerView.adapter = adapter
|
||||
(recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
|
||||
progressBar.hide()
|
||||
statusView.hide()
|
||||
|
||||
initSwipeToRefresh()
|
||||
|
||||
viewModel.conversations.observe(this, Observer<PagedList<ConversationEntity>> {
|
||||
adapter.submitList(it)
|
||||
})
|
||||
viewModel.networkState.observe(this, Observer {
|
||||
adapter.setNetworkState(it)
|
||||
})
|
||||
|
||||
viewModel.load()
|
||||
|
||||
}
|
||||
|
||||
private fun initSwipeToRefresh() {
|
||||
viewModel.refreshState.observe(this, Observer {
|
||||
swipeRefreshLayout.isRefreshing = it == NetworkState.LOADING
|
||||
})
|
||||
swipeRefreshLayout.setOnRefreshListener {
|
||||
viewModel.refresh()
|
||||
}
|
||||
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
|
||||
swipeRefreshLayout.setProgressBackgroundColorSchemeColor(ThemeUtils.getColor(swipeRefreshLayout.context, android.R.attr.colorBackground))
|
||||
}
|
||||
|
||||
private fun onTopLoaded() {
|
||||
recyclerView.scrollToPosition(0)
|
||||
}
|
||||
|
||||
override fun onReblog(reblog: Boolean, position: Int) {
|
||||
// its impossible to reblog private messages
|
||||
}
|
||||
|
||||
override fun onFavourite(favourite: Boolean, position: Int) {
|
||||
viewModel.favourite(favourite, position)
|
||||
}
|
||||
|
||||
override fun onMore(view: View, position: Int) {
|
||||
viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let {
|
||||
more(it.toStatus(), view, position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View) {
|
||||
viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let {
|
||||
viewMedia(attachmentIndex, it.toStatus(), view)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewThread(position: Int) {
|
||||
viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let {
|
||||
viewThread(it.toStatus())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOpenReblog(position: Int) {
|
||||
// there are no reblogs in search results
|
||||
}
|
||||
|
||||
override fun onExpandedChange(expanded: Boolean, position: Int) {
|
||||
viewModel.expandHiddenStatus(expanded, position)
|
||||
}
|
||||
|
||||
override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
|
||||
viewModel.showContent(isShowing, position)
|
||||
}
|
||||
|
||||
override fun onLoadMore(position: Int) {
|
||||
// not using the old way of pagination
|
||||
}
|
||||
|
||||
override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) {
|
||||
viewModel.collapseLongStatus(isCollapsed, position)
|
||||
}
|
||||
|
||||
override fun onViewAccount(id: String) {
|
||||
val intent = AccountActivity.getIntent(requireContext(), id)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onViewTag(tag: String) {
|
||||
val intent = Intent(context, ViewTagActivity::class.java)
|
||||
intent.putExtra("hashtag", tag)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun timelineCases(): TimelineCases {
|
||||
return timelineCases
|
||||
}
|
||||
|
||||
override fun removeItem(position: Int) {
|
||||
viewModel.remove(position)
|
||||
}
|
||||
|
||||
override fun onReply(position: Int) {
|
||||
viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let {
|
||||
reply(it.toStatus())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance() = ConversationsFragment()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
package com.keylesspalace.tusky.components.conversation
|
||||
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.paging.Config
|
||||
import androidx.paging.toLiveData
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.entity.Conversation
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.Listing
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.util.concurrent.Executors
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ConversationsRepository @Inject constructor(val mastodonApi: MastodonApi, val db: AppDatabase) {
|
||||
|
||||
private val ioExecutor = Executors.newSingleThreadExecutor()
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_PAGE_SIZE = 20
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun refresh(accountId: Long, showLoadingIndicator: Boolean): LiveData<NetworkState> {
|
||||
val networkState = MutableLiveData<NetworkState>()
|
||||
if(showLoadingIndicator) {
|
||||
networkState.value = NetworkState.LOADING
|
||||
}
|
||||
|
||||
mastodonApi.getConversations(null, DEFAULT_PAGE_SIZE).enqueue(
|
||||
object : Callback<List<Conversation>> {
|
||||
override fun onFailure(call: Call<List<Conversation>>, t: Throwable) {
|
||||
// retrofit calls this on main thread so safe to call set value
|
||||
networkState.value = NetworkState.error(t.message)
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call<List<Conversation>>, response: Response<List<Conversation>>) {
|
||||
ioExecutor.execute {
|
||||
db.runInTransaction {
|
||||
db.conversationDao().deleteForAccount(accountId)
|
||||
insertResultIntoDb(accountId, response.body())
|
||||
}
|
||||
// since we are in bg thread now, post the result.
|
||||
networkState.postValue(NetworkState.LOADED)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
return networkState
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun conversations(accountId: Long): Listing<ConversationEntity> {
|
||||
// create a boundary callback which will observe when the user reaches to the edges of
|
||||
// the list and update the database with extra data.
|
||||
val boundaryCallback = ConversationsBoundaryCallback(
|
||||
accountId = accountId,
|
||||
mastodonApi = mastodonApi,
|
||||
handleResponse = this::insertResultIntoDb,
|
||||
ioExecutor = ioExecutor,
|
||||
networkPageSize = DEFAULT_PAGE_SIZE)
|
||||
// we are using a mutable live data to trigger refresh requests which eventually calls
|
||||
// refresh method and gets a new live data. Each refresh request by the user becomes a newly
|
||||
// dispatched data in refreshTrigger
|
||||
val refreshTrigger = MutableLiveData<Unit>()
|
||||
val refreshState = Transformations.switchMap(refreshTrigger) {
|
||||
refresh(accountId, true)
|
||||
}
|
||||
|
||||
// We use toLiveData Kotlin extension function here, you could also use LivePagedListBuilder
|
||||
val livePagedList = db.conversationDao().conversationsForAccount(accountId).toLiveData(
|
||||
config = Config(pageSize = DEFAULT_PAGE_SIZE, prefetchDistance = DEFAULT_PAGE_SIZE / 2, enablePlaceholders = false),
|
||||
boundaryCallback = boundaryCallback
|
||||
)
|
||||
|
||||
return Listing(
|
||||
pagedList = livePagedList,
|
||||
networkState = boundaryCallback.networkState,
|
||||
retry = {
|
||||
boundaryCallback.helper.retryAllFailed()
|
||||
},
|
||||
refresh = {
|
||||
refreshTrigger.value = null
|
||||
},
|
||||
refreshState = refreshState
|
||||
)
|
||||
}
|
||||
|
||||
private fun insertResultIntoDb(accountId: Long, result: List<Conversation>?) {
|
||||
result?.let { conversations ->
|
||||
db.conversationDao().insert(conversations.map { it.toEntity(accountId) })
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
package com.keylesspalace.tusky.components.conversation
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.paging.PagedList
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.network.TimelineCases
|
||||
import com.keylesspalace.tusky.util.Listing
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.rxkotlin.addTo
|
||||
import javax.inject.Inject
|
||||
|
||||
class ConversationsViewModel @Inject constructor(
|
||||
private val repository: ConversationsRepository,
|
||||
private val timelineCases: TimelineCases,
|
||||
private val database: AppDatabase,
|
||||
private val accountManager: AccountManager
|
||||
): ViewModel() {
|
||||
|
||||
private val repoResult = MutableLiveData<Listing<ConversationEntity>>()
|
||||
|
||||
val conversations: LiveData<PagedList<ConversationEntity>> = Transformations.switchMap(repoResult) { it.pagedList }
|
||||
val networkState: LiveData<NetworkState> = Transformations.switchMap(repoResult) { it.networkState }
|
||||
val refreshState: LiveData<NetworkState> = Transformations.switchMap(repoResult) { it.refreshState }
|
||||
|
||||
private val disposables = CompositeDisposable()
|
||||
|
||||
fun load() {
|
||||
val accountId = accountManager.activeAccount?.id ?: return
|
||||
if(repoResult.value == null) {
|
||||
repository.refresh(accountId, false)
|
||||
}
|
||||
repoResult.value = repository.conversations(accountId)
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
repoResult.value?.refresh?.invoke()
|
||||
}
|
||||
|
||||
fun retry() {
|
||||
repoResult.value?.retry?.invoke()
|
||||
}
|
||||
|
||||
fun favourite(favourite: Boolean, position: Int) {
|
||||
conversations.value?.getOrNull(position)?.let { conversation ->
|
||||
timelineCases.favourite(conversation.lastStatus.toStatus(), favourite)
|
||||
.subscribe({
|
||||
val newConversation = conversation.copy(
|
||||
lastStatus = conversation.lastStatus.copy(favourited = favourite)
|
||||
)
|
||||
database.conversationDao().insert(newConversation)
|
||||
}, { t -> Log.w("ConversationViewModel", "Failed to favourite conversation", t) })
|
||||
.addTo(disposables)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun expandHiddenStatus(expanded: Boolean, position: Int) {
|
||||
conversations.value?.getOrNull(position)?.let { conversation ->
|
||||
|
||||
val newConversation = conversation.copy(
|
||||
lastStatus = conversation.lastStatus.copy(expanded = expanded)
|
||||
)
|
||||
database.conversationDao().insert(newConversation)
|
||||
}
|
||||
}
|
||||
|
||||
fun collapseLongStatus(collapsed: Boolean, position: Int) {
|
||||
conversations.value?.getOrNull(position)?.let { conversation ->
|
||||
val newConversation = conversation.copy(
|
||||
lastStatus = conversation.lastStatus.copy(collapsed = collapsed)
|
||||
)
|
||||
database.conversationDao().insert(newConversation)
|
||||
}
|
||||
}
|
||||
|
||||
fun showContent(showing: Boolean, position: Int) {
|
||||
conversations.value?.getOrNull(position)?.let { conversation ->
|
||||
val newConversation = conversation.copy(
|
||||
lastStatus = conversation.lastStatus.copy(showingHiddenContent = showing)
|
||||
)
|
||||
database.conversationDao().insert(newConversation)
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(position: Int) {
|
||||
conversations.value?.getOrNull(position)?.let { conversation ->
|
||||
/* this is not ideal since deleting last toot from an conversation
|
||||
should not delete the conversation but show another toot of the conversation */
|
||||
timelineCases.delete(conversation.lastStatus.id)
|
||||
database.conversationDao().delete(conversation)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.dispose()
|
||||
}
|
||||
|
||||
}
|
|
@ -19,6 +19,8 @@ import androidx.room.Entity
|
|||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.TypeConverters
|
||||
import com.keylesspalace.tusky.TabData
|
||||
import com.keylesspalace.tusky.defaultTabs
|
||||
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
|
@ -48,7 +50,8 @@ data class AccountEntity(@field:PrimaryKey(autoGenerate = true) var id: Long,
|
|||
var mediaPreviewEnabled: Boolean = true,
|
||||
var lastNotificationId: String = "0",
|
||||
var activeNotifications: String = "[]",
|
||||
var emojis: List<Emoji> = emptyList()) {
|
||||
var emojis: List<Emoji> = emptyList(),
|
||||
var tabPreferences: List<TabData> = defaultTabs()) {
|
||||
|
||||
val identifier: String
|
||||
get() = "$domain:$accountId"
|
||||
|
|
|
@ -15,6 +15,9 @@
|
|||
|
||||
package com.keylesspalace.tusky.db;
|
||||
|
||||
import com.keylesspalace.tusky.TabDataKt;
|
||||
import com.keylesspalace.tusky.components.conversation.ConversationEntity;
|
||||
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
import androidx.room.Database;
|
||||
import androidx.room.RoomDatabase;
|
||||
|
@ -25,14 +28,15 @@ import androidx.annotation.NonNull;
|
|||
* DB version & declare DAO
|
||||
*/
|
||||
|
||||
@Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class,TimelineStatusEntity.class,
|
||||
TimelineAccountEntity.class
|
||||
}, version = 11)
|
||||
@Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
||||
TimelineAccountEntity.class, ConversationEntity.class
|
||||
}, version = 12)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
|
||||
public abstract TootDao tootDao();
|
||||
public abstract AccountDao accountDao();
|
||||
public abstract InstanceDao instanceDao();
|
||||
public abstract ConversationsDao conversationDao();
|
||||
public abstract TimelineDao timelineDao();
|
||||
|
||||
public static final Migration MIGRATION_2_3 = new Migration(2, 3) {
|
||||
|
@ -166,4 +170,41 @@ public abstract class AppDatabase extends RoomDatabase {
|
|||
}
|
||||
};
|
||||
|
||||
public static final Migration MIGRATION_11_12 = new Migration(11, 12) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||
String defaultTabs = TabDataKt.HOME + ";" +
|
||||
TabDataKt.NOTIFICATIONS + ";" +
|
||||
TabDataKt.LOCAL + ";" +
|
||||
TabDataKt.FEDERATED;
|
||||
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `tabPreferences` TEXT NOT NULL DEFAULT '" + defaultTabs + "'");
|
||||
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `ConversationEntity` (" +
|
||||
"`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_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, " +
|
||||
"PRIMARY KEY(`id`, `accountId`))");
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/* Copyright 2018 Conny Duck
|
||||
*
|
||||
* 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.*
|
||||
import com.keylesspalace.tusky.components.conversation.ConversationEntity
|
||||
|
||||
@Dao
|
||||
interface ConversationsDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(conversations: List<ConversationEntity>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(conversation: ConversationEntity)
|
||||
|
||||
@Delete
|
||||
fun delete(conversation: ConversationEntity)
|
||||
|
||||
@Query("SELECT * FROM ConversationEntity WHERE accountId = :accountId ORDER BY s_createdAt DESC")
|
||||
fun conversationsForAccount(accountId: Long) : DataSource.Factory<Int, ConversationEntity>
|
||||
|
||||
@Query("DELETE FROM ConversationEntity WHERE accountId = :accountId")
|
||||
fun deleteForAccount(accountId: Long)
|
||||
|
||||
|
||||
}
|
|
@ -15,15 +15,25 @@
|
|||
|
||||
package com.keylesspalace.tusky.db
|
||||
|
||||
import android.text.Spanned
|
||||
import androidx.room.TypeConverter
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.keylesspalace.tusky.TabData
|
||||
import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity
|
||||
import com.keylesspalace.tusky.createTabDataFromId
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.json.SpannedTypeAdapter
|
||||
import com.keylesspalace.tusky.util.HtmlUtils
|
||||
import java.util.*
|
||||
|
||||
class Converters {
|
||||
|
||||
private val gson = Gson()
|
||||
private val gson = GsonBuilder()
|
||||
.registerTypeAdapter(Spanned::class.java, SpannedTypeAdapter())
|
||||
.create()
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToEmojiList(emojiListJson: String?): List<Emoji>? {
|
||||
|
@ -36,12 +46,90 @@ class Converters {
|
|||
}
|
||||
|
||||
@TypeConverter
|
||||
fun visibilityToInt(visibility: Status.Visibility): Int {
|
||||
return visibility.num
|
||||
fun visibilityToInt(visibility: Status.Visibility?): Int {
|
||||
return visibility?.num ?: Status.Visibility.UNKNOWN.num
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun intToVisibility(visibility: Int): Status.Visibility {
|
||||
return Status.Visibility.byNum(visibility)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun stringToTabData(str: String?): List<TabData>? {
|
||||
return str?.split(";")
|
||||
?.map { createTabDataFromId(it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun tabDataToString(tabData: List<TabData>?): String? {
|
||||
return tabData?.joinToString(";") { it.id }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun accountToJson(account: ConversationAccountEntity?): String {
|
||||
return gson.toJson(account)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToAccount(accountJson: String?): ConversationAccountEntity? {
|
||||
return gson.fromJson(accountJson, ConversationAccountEntity::class.java)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun accountListToJson(accountList: List<ConversationAccountEntity>?): String {
|
||||
return gson.toJson(accountList)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToAccountList(accountListJson: String?): List<ConversationAccountEntity>? {
|
||||
return gson.fromJson(accountListJson, object : TypeToken<List<ConversationAccountEntity>>() {}.type)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun attachmentListToJson(attachmentList: List<Attachment>?): String {
|
||||
return gson.toJson(attachmentList)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToAttachmentList(attachmentListJson: String?): List<Attachment>? {
|
||||
return gson.fromJson(attachmentListJson, object : TypeToken<List<Attachment>>() {}.type)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun mentionArrayToJson(mentionArray: Array<Status.Mention>?): String? {
|
||||
return gson.toJson(mentionArray)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToMentionArray(mentionListJson: String?): Array<Status.Mention>? {
|
||||
return gson.fromJson(mentionListJson, object : TypeToken<Array<Status.Mention>>() {}.type)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun dateToLong(date: Date): Long {
|
||||
return date.time
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun longToDate(date: Long): Date {
|
||||
return Date(date)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun spannedToString(spanned: Spanned?): String? {
|
||||
if(spanned == null) {
|
||||
return null
|
||||
}
|
||||
return HtmlUtils.toHtml(spanned)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun stringToSpanned(spannedString: String?): Spanned? {
|
||||
if(spannedString == null) {
|
||||
return null
|
||||
}
|
||||
return HtmlUtils.fromHtml(spannedString)
|
||||
}
|
||||
|
||||
}
|
|
@ -86,4 +86,7 @@ abstract class ActivitiesModule {
|
|||
@ContributesAndroidInjector
|
||||
abstract fun contributesLicenseActivity(): LicenseActivity
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributesTabPreferenceActivity(): TabPreferenceActivity
|
||||
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package com.keylesspalace.tusky.di
|
||||
|
||||
import com.keylesspalace.tusky.components.conversation.ConversationsFragment
|
||||
import com.keylesspalace.tusky.fragment.*
|
||||
import com.keylesspalace.tusky.fragment.preference.*
|
||||
import dagger.Module
|
||||
|
@ -51,4 +52,7 @@ abstract class FragmentBuildersModule {
|
|||
@ContributesAndroidInjector
|
||||
abstract fun accountPreferencesFragment(): AccountPreferencesFragment
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun directMessagesPreferencesFragment(): ConversationsFragment
|
||||
|
||||
}
|
|
@ -46,6 +46,7 @@ import javax.inject.Singleton
|
|||
|
||||
@Module
|
||||
class NetworkModule {
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ClassKey(Spanned::class)
|
||||
|
|
|
@ -5,6 +5,7 @@ package com.keylesspalace.tusky.di
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.keylesspalace.tusky.viewmodel.AccountViewModel
|
||||
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
|
||||
import com.keylesspalace.tusky.viewmodel.EditProfileViewModel
|
||||
import dagger.Binds
|
||||
import dagger.MapKey
|
||||
|
@ -42,5 +43,10 @@ abstract class ViewModelModule {
|
|||
@ViewModelKey(EditProfileViewModel::class)
|
||||
internal abstract fun editProfileViewModel(viewModel: EditProfileViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(ConversationsViewModel::class)
|
||||
internal abstract fun conversationsViewModel(viewModel: ConversationsViewModel): ViewModel
|
||||
|
||||
//Add more ViewModels here
|
||||
}
|
|
@ -37,13 +37,13 @@ data class Account(
|
|||
val avatar: String,
|
||||
val header: String,
|
||||
val locked: Boolean = false,
|
||||
@SerializedName("followers_count") val followersCount: Int,
|
||||
@SerializedName("following_count") val followingCount: Int,
|
||||
@SerializedName("statuses_count") val statusesCount: Int,
|
||||
val source: AccountSource?,
|
||||
val bot: Boolean,
|
||||
val emojis: List<Emoji>?, // nullable for backward compatibility
|
||||
val fields: List<Field>?, //nullable for backward compatibility
|
||||
@SerializedName("followers_count") val followersCount: Int = 0,
|
||||
@SerializedName("following_count") val followingCount: Int = 0,
|
||||
@SerializedName("statuses_count") val statusesCount: Int = 0,
|
||||
val source: AccountSource? = null,
|
||||
val bot: Boolean = false,
|
||||
val emojis: List<Emoji>? = emptyList(), // nullable for backward compatibility
|
||||
val fields: List<Field>? = emptyList(), //nullable for backward compatibility
|
||||
val moved: Account? = null
|
||||
|
||||
) : Parcelable {
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/* Copyright 2019 Conny Duck
|
||||
*
|
||||
* 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.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class Conversation(
|
||||
val id: String,
|
||||
val accounts: List<Account>,
|
||||
@SerializedName("last_status") val lastStatus: Status,
|
||||
val unread: Boolean
|
||||
)
|
|
@ -42,7 +42,7 @@ data class Status(
|
|||
var pinned: Boolean?
|
||||
) {
|
||||
|
||||
val actionableId: String?
|
||||
val actionableId: String
|
||||
get() = reblog?.id ?: id
|
||||
|
||||
val actionableStatus: Status
|
||||
|
|
|
@ -72,10 +72,10 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable {
|
|||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
recyclerView.setHasFixedSize(true)
|
||||
val layoutManager = LinearLayoutManager(context)
|
||||
val layoutManager = LinearLayoutManager(view.context)
|
||||
recyclerView.layoutManager = layoutManager
|
||||
val divider = DividerItemDecoration(context, layoutManager.orientation)
|
||||
val drawable = ThemeUtils.getDrawable(context, R.attr.status_divider_drawable, R.drawable.status_divider_dark)
|
||||
val divider = DividerItemDecoration(view.context, layoutManager.orientation)
|
||||
val drawable = ThemeUtils.getDrawable(view.context, R.attr.status_divider_drawable, R.drawable.status_divider_dark)
|
||||
divider.setDrawable(drawable)
|
||||
recyclerView.addItemDecoration(divider)
|
||||
|
||||
|
|
|
@ -81,9 +81,10 @@ class AccountMediaFragment : BaseFragment(), Injectable {
|
|||
private val callback = object : Callback<List<Status>> {
|
||||
override fun onFailure(call: Call<List<Status>>?, t: Throwable?) {
|
||||
fetchingStatus = FetchingStatus.NOT_FETCHING
|
||||
if (isAdded) {
|
||||
swipe_refresh_layout.isRefreshing = false
|
||||
progress_bar.visibility = View.GONE
|
||||
|
||||
if(isAdded) {
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
progressBar.visibility = View.GONE
|
||||
statusView.show()
|
||||
if (t is IOException) {
|
||||
statusView.setup(R.drawable.elephant_offline, R.string.error_network) {
|
||||
|
@ -101,9 +102,9 @@ class AccountMediaFragment : BaseFragment(), Injectable {
|
|||
|
||||
override fun onResponse(call: Call<List<Status>>, response: Response<List<Status>>) {
|
||||
fetchingStatus = FetchingStatus.NOT_FETCHING
|
||||
if (isAdded) {
|
||||
swipe_refresh_layout.isRefreshing = false
|
||||
progress_bar.visibility = View.GONE
|
||||
if(isAdded) {
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
progressBar.visibility = View.GONE
|
||||
|
||||
val body = response.body()
|
||||
body?.let { fetched ->
|
||||
|
@ -114,6 +115,7 @@ class AccountMediaFragment : BaseFragment(), Injectable {
|
|||
result.addAll(AttachmentViewData.list(status))
|
||||
}
|
||||
adapter.addTop(result)
|
||||
|
||||
if (statuses.isEmpty()) {
|
||||
statusView.show()
|
||||
statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty,
|
||||
|
@ -159,19 +161,19 @@ class AccountMediaFragment : BaseFragment(), Injectable {
|
|||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
|
||||
val columnCount = context?.resources?.getInteger(R.integer.profile_media_column_count) ?: 2
|
||||
val layoutManager = GridLayoutManager(context, columnCount)
|
||||
val columnCount = view.context.resources.getInteger(R.integer.profile_media_column_count)
|
||||
val layoutManager = GridLayoutManager(view.context, columnCount)
|
||||
|
||||
val bgRes = ThemeUtils.getColorId(context, R.attr.window_background)
|
||||
val bgRes = ThemeUtils.getColorId(view.context, R.attr.window_background)
|
||||
|
||||
adapter.baseItemColor = ContextCompat.getColor(recycler_view.context, bgRes)
|
||||
adapter.baseItemColor = ContextCompat.getColor(recyclerView.context, bgRes)
|
||||
|
||||
recycler_view.layoutManager = layoutManager
|
||||
recycler_view.adapter = adapter
|
||||
recyclerView.layoutManager = layoutManager
|
||||
recyclerView.adapter = adapter
|
||||
|
||||
val accountId = arguments?.getString(ACCOUNT_ID_ARG)
|
||||
|
||||
swipe_refresh_layout.setOnRefreshListener {
|
||||
swipeRefreshLayout.setOnRefreshListener {
|
||||
statusView.hide()
|
||||
if (fetchingStatus != FetchingStatus.NOT_FETCHING) return@setOnRefreshListener
|
||||
currentCall = if (statuses.isEmpty()) {
|
||||
|
@ -184,12 +186,12 @@ class AccountMediaFragment : BaseFragment(), Injectable {
|
|||
currentCall?.enqueue(callback)
|
||||
|
||||
}
|
||||
swipe_refresh_layout.setColorSchemeResources(R.color.tusky_blue)
|
||||
swipe_refresh_layout.setProgressBackgroundColorSchemeColor(ThemeUtils.getColor(context, android.R.attr.colorBackground))
|
||||
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
|
||||
swipeRefreshLayout.setProgressBackgroundColorSchemeColor(ThemeUtils.getColor(view.context, android.R.attr.colorBackground))
|
||||
|
||||
statusView.visibility = View.GONE
|
||||
|
||||
recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
|
||||
override fun onScrolled(recycler_view: RecyclerView, dx: Int, dy: Int) {
|
||||
if (dy > 0) {
|
||||
|
|
|
@ -176,9 +176,9 @@ public class NotificationsFragment extends SFragment implements
|
|||
|
||||
@NonNull Context context = inflater.getContext(); // from inflater to silence warning
|
||||
// Setup the SwipeRefreshLayout.
|
||||
swipeRefreshLayout = rootView.findViewById(R.id.swipe_refresh_layout);
|
||||
recyclerView = rootView.findViewById(R.id.recycler_view);
|
||||
progressBar = rootView.findViewById(R.id.progress_bar);
|
||||
swipeRefreshLayout = rootView.findViewById(R.id.swipeRefreshLayout);
|
||||
recyclerView = rootView.findViewById(R.id.recyclerView);
|
||||
progressBar = rootView.findViewById(R.id.progressBar);
|
||||
statusView = rootView.findViewById(R.id.statusView);
|
||||
|
||||
swipeRefreshLayout.setOnRefreshListener(this);
|
||||
|
@ -417,13 +417,13 @@ public class NotificationsFragment extends SFragment implements
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onMore(View view, int position) {
|
||||
public void onMore(@NonNull View view, int position) {
|
||||
Notification notification = notifications.get(position).asRight();
|
||||
super.more(notification.getStatus(), view, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewMedia(int position, int attachmentIndex, View view) {
|
||||
public void onViewMedia(int position, int attachmentIndex, @NonNull View view) {
|
||||
Notification notification = notifications.get(position).asRightOrNull();
|
||||
if (notification == null || notification.getStatus() == null) return;
|
||||
super.viewMedia(attachmentIndex, notification.getStatus(), view);
|
||||
|
|
|
@ -98,7 +98,7 @@ public abstract class SFragment extends BaseFragment {
|
|||
}
|
||||
|
||||
protected void viewThread(Status status) {
|
||||
bottomSheetActivity.viewThread(status);
|
||||
bottomSheetActivity.viewThread(status.getActionableId(), status.getUrl());
|
||||
}
|
||||
|
||||
protected void viewAccount(String accountId) {
|
||||
|
|
|
@ -182,14 +182,14 @@ class SearchFragment : SFragment(), StatusActionListener, Injectable {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onMore(view: View?, position: Int) {
|
||||
override fun onMore(view: View, position: Int) {
|
||||
val status = searchAdapter.getStatusAtPosition(position)
|
||||
if (status != null) {
|
||||
more(status, view, position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
|
||||
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View) {
|
||||
val status = searchAdapter.getStatusAtPosition(position) ?: return
|
||||
viewMedia(attachmentIndex, status, view)
|
||||
}
|
||||
|
|
|
@ -219,9 +219,9 @@ public class TimelineFragment extends SFragment implements
|
|||
Bundle savedInstanceState) {
|
||||
final View rootView = inflater.inflate(R.layout.fragment_timeline, container, false);
|
||||
|
||||
recyclerView = rootView.findViewById(R.id.recycler_view);
|
||||
swipeRefreshLayout = rootView.findViewById(R.id.swipe_refresh_layout);
|
||||
progressBar = rootView.findViewById(R.id.progress_bar);
|
||||
recyclerView = rootView.findViewById(R.id.recyclerView);
|
||||
swipeRefreshLayout = rootView.findViewById(R.id.swipeRefreshLayout);
|
||||
progressBar = rootView.findViewById(R.id.progressBar);
|
||||
statusView = rootView.findViewById(R.id.statusView);
|
||||
|
||||
setupSwipeRefreshLayout();
|
||||
|
@ -608,7 +608,7 @@ public class TimelineFragment extends SFragment implements
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onMore(View view, final int position) {
|
||||
public void onMore(@NonNull View view, final int position) {
|
||||
super.more(statuses.get(position).asRight(), view, position);
|
||||
}
|
||||
|
||||
|
@ -689,7 +689,7 @@ public class TimelineFragment extends SFragment implements
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onViewMedia(int position, int attachmentIndex, View view) {
|
||||
public void onViewMedia(int position, int attachmentIndex, @NonNull View view) {
|
||||
Status status = statuses.get(position).asRightOrNull();
|
||||
if (status == null) return;
|
||||
super.viewMedia(attachmentIndex, status, view);
|
||||
|
|
|
@ -137,13 +137,13 @@ public final class ViewThreadFragment extends SFragment implements
|
|||
View rootView = inflater.inflate(R.layout.fragment_view_thread, container, false);
|
||||
|
||||
Context context = getContext();
|
||||
swipeRefreshLayout = rootView.findViewById(R.id.swipe_refresh_layout);
|
||||
swipeRefreshLayout = rootView.findViewById(R.id.swipeRefreshLayout);
|
||||
swipeRefreshLayout.setOnRefreshListener(this);
|
||||
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue);
|
||||
swipeRefreshLayout.setProgressBackgroundColorSchemeColor(
|
||||
ThemeUtils.getColor(context, android.R.attr.colorBackground));
|
||||
|
||||
recyclerView = rootView.findViewById(R.id.recycler_view);
|
||||
recyclerView = rootView.findViewById(R.id.recyclerView);
|
||||
recyclerView.setHasFixedSize(true);
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(context);
|
||||
recyclerView.setLayoutManager(layoutManager);
|
||||
|
@ -284,12 +284,12 @@ public final class ViewThreadFragment extends SFragment implements
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onMore(View view, int position) {
|
||||
public void onMore(@NonNull View view, int position) {
|
||||
super.more(statuses.get(position), view, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewMedia(int position, int attachmentIndex, View view) {
|
||||
public void onViewMedia(int position, int attachmentIndex, @NonNull View view) {
|
||||
Status status = statuses.get(position);
|
||||
super.viewMedia(attachmentIndex, status, view);
|
||||
}
|
||||
|
|
|
@ -26,10 +26,7 @@ import androidx.preference.Preference
|
|||
import androidx.preference.PreferenceFragmentCompat
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import com.keylesspalace.tusky.AccountListActivity
|
||||
import com.keylesspalace.tusky.BuildConfig
|
||||
import com.keylesspalace.tusky.PreferencesActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.*
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
|
@ -60,6 +57,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
|||
lateinit var eventHub: EventHub
|
||||
|
||||
private lateinit var notificationPreference: Preference
|
||||
private lateinit var tabPreference: Preference
|
||||
private lateinit var mutedUsersPreference: Preference
|
||||
private lateinit var blockedUsersPreference: Preference
|
||||
|
||||
|
@ -74,6 +72,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
|||
addPreferencesFromResource(R.xml.account_preferences)
|
||||
|
||||
notificationPreference = findPreference("notificationPreference")
|
||||
tabPreference = findPreference("tabPreference")
|
||||
mutedUsersPreference = findPreference("mutedUsersPreference")
|
||||
blockedUsersPreference = findPreference("blockedUsersPreference")
|
||||
defaultPostPrivacyPreference = findPreference("defaultPostPrivacy") as ListPreference
|
||||
|
@ -81,11 +80,12 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
|||
mediaPreviewEnabledPreference = findPreference("mediaPreviewEnabled") as SwitchPreference
|
||||
alwaysShowSensitiveMediaPreference = findPreference("alwaysShowSensitiveMedia") as SwitchPreference
|
||||
|
||||
notificationPreference.icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_notifications).sizePx(iconSize).color(ThemeUtils.getColor(context, R.attr.toolbar_icon_tint))
|
||||
notificationPreference.icon = IconicsDrawable(notificationPreference.context, GoogleMaterial.Icon.gmd_notifications).sizePx(iconSize).color(ThemeUtils.getColor(notificationPreference.context, R.attr.toolbar_icon_tint))
|
||||
mutedUsersPreference.icon = getTintedIcon(R.drawable.ic_mute_24dp)
|
||||
blockedUsersPreference.icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_block).sizePx(iconSize).color(ThemeUtils.getColor(context, R.attr.toolbar_icon_tint))
|
||||
blockedUsersPreference.icon = IconicsDrawable(blockedUsersPreference.context, GoogleMaterial.Icon.gmd_block).sizePx(iconSize).color(ThemeUtils.getColor(blockedUsersPreference.context, R.attr.toolbar_icon_tint))
|
||||
|
||||
notificationPreference.onPreferenceClickListener = this
|
||||
tabPreference.onPreferenceClickListener = this
|
||||
mutedUsersPreference.onPreferenceClickListener = this
|
||||
blockedUsersPreference.onPreferenceClickListener = this
|
||||
|
||||
|
@ -161,6 +161,12 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(),
|
|||
}
|
||||
return true
|
||||
}
|
||||
tabPreference -> {
|
||||
val intent = Intent(context, TabPreferenceActivity::class.java)
|
||||
activity?.startActivity(intent)
|
||||
activity?.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
|
||||
return true
|
||||
}
|
||||
mutedUsersPreference -> {
|
||||
val intent = Intent(context, AccountListActivity::class.java)
|
||||
intent.putExtra("type", AccountListActivity.Type.MUTES)
|
||||
|
|
|
@ -34,13 +34,13 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
|||
addPreferencesFromResource(R.xml.preferences)
|
||||
|
||||
val themePreference: Preference = findPreference("appTheme")
|
||||
themePreference.icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_palette).sizePx(iconSize).color(ThemeUtils.getColor(context, R.attr.toolbar_icon_tint))
|
||||
themePreference.icon = IconicsDrawable(themePreference.context, GoogleMaterial.Icon.gmd_palette).sizePx(iconSize).color(ThemeUtils.getColor(themePreference.context, R.attr.toolbar_icon_tint))
|
||||
|
||||
val emojiPreference: Preference = findPreference("emojiCompat")
|
||||
emojiPreference.icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_sentiment_satisfied).sizePx(iconSize).color(ThemeUtils.getColor(context, R.attr.toolbar_icon_tint))
|
||||
emojiPreference.icon = IconicsDrawable(emojiPreference.context, GoogleMaterial.Icon.gmd_sentiment_satisfied).sizePx(iconSize).color(ThemeUtils.getColor(emojiPreference.context, R.attr.toolbar_icon_tint))
|
||||
|
||||
val textSizePreference: Preference = findPreference("statusTextSize")
|
||||
textSizePreference.icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_format_size).sizePx(iconSize).color(ThemeUtils.getColor(context, R.attr.toolbar_icon_tint))
|
||||
textSizePreference.icon = IconicsDrawable(textSizePreference.context, GoogleMaterial.Icon.gmd_format_size).sizePx(iconSize).color(ThemeUtils.getColor(textSizePreference.context, R.attr.toolbar_icon_tint))
|
||||
|
||||
val timelineFilterPreferences: Preference = findPreference("timelineFilterPreferences")
|
||||
timelineFilterPreferences.setOnPreferenceClickListener {
|
||||
|
|
|
@ -17,12 +17,14 @@ package com.keylesspalace.tusky.interfaces;
|
|||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public interface StatusActionListener extends LinkListener {
|
||||
void onReply(int position);
|
||||
void onReblog(final boolean reblog, final int position);
|
||||
void onFavourite(final boolean favourite, final int position);
|
||||
void onMore(View view, final int position);
|
||||
void onViewMedia(int position, int attachmentIndex, View view);
|
||||
void onMore(@NonNull View view, final int position);
|
||||
void onViewMedia(int position, int attachmentIndex, @NonNull View view);
|
||||
void onViewThread(int position);
|
||||
void onOpenReblog(int position);
|
||||
void onExpandedChange(boolean expanded, int position);
|
||||
|
|
|
@ -22,11 +22,14 @@ import com.google.gson.JsonDeserializationContext;
|
|||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
import com.keylesspalace.tusky.util.HtmlUtils;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
public class SpannedTypeAdapter implements JsonDeserializer<Spanned> {
|
||||
public class SpannedTypeAdapter implements JsonDeserializer<Spanned>, JsonSerializer<Spanned> {
|
||||
@Override
|
||||
public Spanned deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
|
||||
throws JsonParseException {
|
||||
|
@ -37,4 +40,9 @@ public class SpannedTypeAdapter implements JsonDeserializer<Spanned> {
|
|||
return new SpannedString("");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(Spanned src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return new JsonPrimitive(HtmlUtils.toHtml(src));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import com.keylesspalace.tusky.entity.Account;
|
|||
import com.keylesspalace.tusky.entity.AppCredentials;
|
||||
import com.keylesspalace.tusky.entity.Attachment;
|
||||
import com.keylesspalace.tusky.entity.Card;
|
||||
import com.keylesspalace.tusky.entity.Conversation;
|
||||
import com.keylesspalace.tusky.entity.Emoji;
|
||||
import com.keylesspalace.tusky.entity.Instance;
|
||||
import com.keylesspalace.tusky.entity.MastoList;
|
||||
|
@ -317,4 +318,7 @@ public interface MastodonApi {
|
|||
|
||||
@GET("api/v1/instance")
|
||||
Call<Instance> getInstance();
|
||||
|
||||
@GET("/api/v1/conversations")
|
||||
Call<List<Conversation>> getConversations(@Nullable @Query("max_id") String maxId, @Query("limit") int limit);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/* 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.pager
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
import androidx.viewpager.widget.PagerAdapter
|
||||
import com.keylesspalace.tusky.TabData
|
||||
|
||||
class MainPagerAdapter(val tabs: List<TabData>, manager: FragmentManager) : FragmentPagerAdapter(manager) {
|
||||
|
||||
override fun getItem(position: Int): Fragment {
|
||||
return tabs[position].fragment()
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
return tabs.size
|
||||
}
|
||||
|
||||
override fun getPageTitle(position: Int): CharSequence? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return tabs[position].id.hashCode().toLong()
|
||||
}
|
||||
|
||||
override fun getItemPosition(item: Any): Int {
|
||||
return PagerAdapter.POSITION_NONE
|
||||
}
|
||||
|
||||
}
|
|
@ -1,60 +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.pager;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
|
||||
import com.keylesspalace.tusky.fragment.NotificationsFragment;
|
||||
import com.keylesspalace.tusky.fragment.TimelineFragment;
|
||||
|
||||
public class TimelinePagerAdapter extends FragmentPagerAdapter {
|
||||
public TimelinePagerAdapter(FragmentManager manager) {
|
||||
super(manager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int i) {
|
||||
switch (i) {
|
||||
case 0: {
|
||||
return TimelineFragment.newInstance(TimelineFragment.Kind.HOME);
|
||||
}
|
||||
case 1: {
|
||||
return NotificationsFragment.newInstance();
|
||||
}
|
||||
case 2: {
|
||||
return TimelineFragment.newInstance(TimelineFragment.Kind.PUBLIC_LOCAL);
|
||||
}
|
||||
case 3: {
|
||||
return TimelineFragment.newInstance(TimelineFragment.Kind.PUBLIC_FEDERATED);
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return 4;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
return null;
|
||||
}
|
||||
}
|
36
app/src/main/java/com/keylesspalace/tusky/util/Listing.kt
Normal file
36
app/src/main/java/com/keylesspalace/tusky/util/Listing.kt
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.keylesspalace.tusky.util
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.paging.PagedList
|
||||
|
||||
/**
|
||||
* Data class that is necessary for a UI to show a listing and interact w/ the rest of the system
|
||||
*/
|
||||
data class Listing<T>(
|
||||
// the LiveData of paged lists for the UI to observe
|
||||
val pagedList: LiveData<PagedList<T>>,
|
||||
// represents the network request status to show to the user
|
||||
val networkState: LiveData<NetworkState>,
|
||||
// represents the refresh status to show to the user. Separate from networkState, this
|
||||
// value is importantly only when refresh is requested.
|
||||
val refreshState: LiveData<NetworkState>,
|
||||
// refreshes the whole data and fetches it from scratch.
|
||||
val refresh: () -> Unit,
|
||||
// retries any failed requests.
|
||||
val retry: () -> Unit)
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.keylesspalace.tusky.util
|
||||
|
||||
enum class Status {
|
||||
RUNNING,
|
||||
SUCCESS,
|
||||
FAILED
|
||||
}
|
||||
|
||||
@Suppress("DataClassPrivateConstructor")
|
||||
data class NetworkState private constructor(
|
||||
val status: Status,
|
||||
val msg: String? = null) {
|
||||
companion object {
|
||||
val LOADED = NetworkState(Status.SUCCESS)
|
||||
val LOADING = NetworkState(Status.RUNNING)
|
||||
fun error(msg: String?) = NetworkState(Status.FAILED, msg)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,491 @@
|
|||
/*
|
||||
* Copyright 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.keylesspalace.tusky.util;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
/**
|
||||
* A helper class for {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}s and
|
||||
* {@link DataSource}s to help with tracking network requests.
|
||||
* <p>
|
||||
* It is designed to support 3 types of requests, {@link RequestType#INITIAL INITIAL},
|
||||
* {@link RequestType#BEFORE BEFORE} and {@link RequestType#AFTER AFTER} and runs only 1 request
|
||||
* for each of them via {@link #runIfNotRunning(RequestType, Request)}.
|
||||
* <p>
|
||||
* It tracks a {@link Status} and an {@code error} for each {@link RequestType}.
|
||||
* <p>
|
||||
* A sample usage of this class to limit requests looks like this:
|
||||
* <pre>
|
||||
* class PagingBoundaryCallback extends PagedList.BoundaryCallback<MyItem> {
|
||||
* // TODO replace with an executor from your application
|
||||
* Executor executor = Executors.newSingleThreadExecutor();
|
||||
* PagingRequestHelper helper = new PagingRequestHelper(executor);
|
||||
* // imaginary API service, using Retrofit
|
||||
* MyApi api;
|
||||
*
|
||||
* {@literal @}Override
|
||||
* public void onItemAtFrontLoaded({@literal @}NonNull MyItem itemAtFront) {
|
||||
* helper.runIfNotRunning(PagingRequestHelper.RequestType.BEFORE,
|
||||
* helperCallback -> api.getTopBefore(itemAtFront.getName(), 10).enqueue(
|
||||
* new Callback<ApiResponse>() {
|
||||
* {@literal @}Override
|
||||
* public void onResponse(Call<ApiResponse> call,
|
||||
* Response<ApiResponse> response) {
|
||||
* // TODO insert new records into database
|
||||
* helperCallback.recordSuccess();
|
||||
* }
|
||||
*
|
||||
* {@literal @}Override
|
||||
* public void onFailure(Call<ApiResponse> call, Throwable t) {
|
||||
* helperCallback.recordFailure(t);
|
||||
* }
|
||||
* }));
|
||||
* }
|
||||
*
|
||||
* {@literal @}Override
|
||||
* public void onItemAtEndLoaded({@literal @}NonNull MyItem itemAtEnd) {
|
||||
* helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER,
|
||||
* helperCallback -> api.getTopBefore(itemAtEnd.getName(), 10).enqueue(
|
||||
* new Callback<ApiResponse>() {
|
||||
* {@literal @}Override
|
||||
* public void onResponse(Call<ApiResponse> call,
|
||||
* Response<ApiResponse> response) {
|
||||
* // TODO insert new records into database
|
||||
* helperCallback.recordSuccess();
|
||||
* }
|
||||
*
|
||||
* {@literal @}Override
|
||||
* public void onFailure(Call<ApiResponse> call, Throwable t) {
|
||||
* helperCallback.recordFailure(t);
|
||||
* }
|
||||
* }));
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
* <p>
|
||||
* The helper provides an API to observe combined request status, which can be reported back to the
|
||||
* application based on your business rules.
|
||||
* <pre>
|
||||
* MutableLiveData<PagingRequestHelper.Status> combined = new MutableLiveData<>();
|
||||
* helper.addListener(status -> {
|
||||
* // merge multiple states per request type into one, or dispatch separately depending on
|
||||
* // your application logic.
|
||||
* if (status.hasRunning()) {
|
||||
* combined.postValue(PagingRequestHelper.Status.RUNNING);
|
||||
* } else if (status.hasError()) {
|
||||
* // can also obtain the error via {@link StatusReport#getErrorFor(RequestType)}
|
||||
* combined.postValue(PagingRequestHelper.Status.FAILED);
|
||||
* } else {
|
||||
* combined.postValue(PagingRequestHelper.Status.SUCCESS);
|
||||
* }
|
||||
* });
|
||||
* </pre>
|
||||
*/
|
||||
// THIS class is likely to be moved into the library in a future release. Feel free to copy it
|
||||
// from this sample.
|
||||
public class PagingRequestHelper {
|
||||
private final Object mLock = new Object();
|
||||
private final Executor mRetryService;
|
||||
@GuardedBy("mLock")
|
||||
private final RequestQueue[] mRequestQueues = new RequestQueue[]
|
||||
{new RequestQueue(RequestType.INITIAL),
|
||||
new RequestQueue(RequestType.BEFORE),
|
||||
new RequestQueue(RequestType.AFTER)};
|
||||
@NonNull
|
||||
final CopyOnWriteArrayList<Listener> mListeners = new CopyOnWriteArrayList<>();
|
||||
/**
|
||||
* Creates a new PagingRequestHelper with the given {@link Executor} which is used to run
|
||||
* retry actions.
|
||||
*
|
||||
* @param retryService The {@link Executor} that can run the retry actions.
|
||||
*/
|
||||
public PagingRequestHelper(@NonNull Executor retryService) {
|
||||
mRetryService = retryService;
|
||||
}
|
||||
/**
|
||||
* Adds a new listener that will be notified when any request changes {@link Status state}.
|
||||
*
|
||||
* @param listener The listener that will be notified each time a request's status changes.
|
||||
* @return True if it is added, false otherwise (e.g. it already exists in the list).
|
||||
*/
|
||||
@AnyThread
|
||||
public boolean addListener(@NonNull Listener listener) {
|
||||
return mListeners.add(listener);
|
||||
}
|
||||
/**
|
||||
* Removes the given listener from the listeners list.
|
||||
*
|
||||
* @param listener The listener that will be removed.
|
||||
* @return True if the listener is removed, false otherwise (e.g. it never existed)
|
||||
*/
|
||||
public boolean removeListener(@NonNull Listener listener) {
|
||||
return mListeners.remove(listener);
|
||||
}
|
||||
/**
|
||||
* Runs the given {@link Request} if no other requests in the given request type is already
|
||||
* running.
|
||||
* <p>
|
||||
* If run, the request will be run in the current thread.
|
||||
*
|
||||
* @param type The type of the request.
|
||||
* @param request The request to run.
|
||||
* @return True if the request is run, false otherwise.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@AnyThread
|
||||
public boolean runIfNotRunning(@NonNull RequestType type, @NonNull Request request) {
|
||||
boolean hasListeners = !mListeners.isEmpty();
|
||||
StatusReport report = null;
|
||||
synchronized (mLock) {
|
||||
RequestQueue queue = mRequestQueues[type.ordinal()];
|
||||
if (queue.mRunning != null) {
|
||||
return false;
|
||||
}
|
||||
queue.mRunning = request;
|
||||
queue.mStatus = Status.RUNNING;
|
||||
queue.mFailed = null;
|
||||
queue.mLastError = null;
|
||||
if (hasListeners) {
|
||||
report = prepareStatusReportLocked();
|
||||
}
|
||||
}
|
||||
if (report != null) {
|
||||
dispatchReport(report);
|
||||
}
|
||||
final RequestWrapper wrapper = new RequestWrapper(request, this, type);
|
||||
wrapper.run();
|
||||
return true;
|
||||
}
|
||||
@GuardedBy("mLock")
|
||||
private StatusReport prepareStatusReportLocked() {
|
||||
Throwable[] errors = new Throwable[]{
|
||||
mRequestQueues[0].mLastError,
|
||||
mRequestQueues[1].mLastError,
|
||||
mRequestQueues[2].mLastError
|
||||
};
|
||||
return new StatusReport(
|
||||
getStatusForLocked(RequestType.INITIAL),
|
||||
getStatusForLocked(RequestType.BEFORE),
|
||||
getStatusForLocked(RequestType.AFTER),
|
||||
errors
|
||||
);
|
||||
}
|
||||
@GuardedBy("mLock")
|
||||
private Status getStatusForLocked(RequestType type) {
|
||||
return mRequestQueues[type.ordinal()].mStatus;
|
||||
}
|
||||
@AnyThread
|
||||
@VisibleForTesting
|
||||
void recordResult(@NonNull RequestWrapper wrapper, @Nullable Throwable throwable) {
|
||||
StatusReport report = null;
|
||||
final boolean success = throwable == null;
|
||||
boolean hasListeners = !mListeners.isEmpty();
|
||||
synchronized (mLock) {
|
||||
RequestQueue queue = mRequestQueues[wrapper.mType.ordinal()];
|
||||
queue.mRunning = null;
|
||||
queue.mLastError = throwable;
|
||||
if (success) {
|
||||
queue.mFailed = null;
|
||||
queue.mStatus = Status.SUCCESS;
|
||||
} else {
|
||||
queue.mFailed = wrapper;
|
||||
queue.mStatus = Status.FAILED;
|
||||
}
|
||||
if (hasListeners) {
|
||||
report = prepareStatusReportLocked();
|
||||
}
|
||||
}
|
||||
if (report != null) {
|
||||
dispatchReport(report);
|
||||
}
|
||||
}
|
||||
private void dispatchReport(StatusReport report) {
|
||||
for (Listener listener : mListeners) {
|
||||
listener.onStatusChange(report);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Retries all failed requests.
|
||||
*
|
||||
* @return True if any request is retried, false otherwise.
|
||||
*/
|
||||
public boolean retryAllFailed() {
|
||||
final RequestWrapper[] toBeRetried = new RequestWrapper[RequestType.values().length];
|
||||
boolean retried = false;
|
||||
synchronized (mLock) {
|
||||
for (int i = 0; i < RequestType.values().length; i++) {
|
||||
toBeRetried[i] = mRequestQueues[i].mFailed;
|
||||
mRequestQueues[i].mFailed = null;
|
||||
}
|
||||
}
|
||||
for (RequestWrapper failed : toBeRetried) {
|
||||
if (failed != null) {
|
||||
failed.retry(mRetryService);
|
||||
retried = true;
|
||||
}
|
||||
}
|
||||
return retried;
|
||||
}
|
||||
static class RequestWrapper implements Runnable {
|
||||
@NonNull
|
||||
final Request mRequest;
|
||||
@NonNull
|
||||
final PagingRequestHelper mHelper;
|
||||
@NonNull
|
||||
final RequestType mType;
|
||||
RequestWrapper(@NonNull Request request, @NonNull PagingRequestHelper helper,
|
||||
@NonNull RequestType type) {
|
||||
mRequest = request;
|
||||
mHelper = helper;
|
||||
mType = type;
|
||||
}
|
||||
@Override
|
||||
public void run() {
|
||||
mRequest.run(new Request.Callback(this, mHelper));
|
||||
}
|
||||
void retry(Executor service) {
|
||||
service.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mHelper.runIfNotRunning(mType, mRequest);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Runner class that runs a request tracked by the {@link PagingRequestHelper}.
|
||||
* <p>
|
||||
* When a request is invoked, it must call one of {@link Callback#recordFailure(Throwable)}
|
||||
* or {@link Callback#recordSuccess()} once and only once. This call
|
||||
* can be made any time. Until that method call is made, {@link PagingRequestHelper} will
|
||||
* consider the request is running.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface Request {
|
||||
/**
|
||||
* Should run the request and call the given {@link Callback} with the result of the
|
||||
* request.
|
||||
*
|
||||
* @param callback The callback that should be invoked with the result.
|
||||
*/
|
||||
void run(Callback callback);
|
||||
/**
|
||||
* Callback class provided to the {@link #run(Callback)} method to report the result.
|
||||
*/
|
||||
class Callback {
|
||||
private final AtomicBoolean mCalled = new AtomicBoolean();
|
||||
private final RequestWrapper mWrapper;
|
||||
private final PagingRequestHelper mHelper;
|
||||
Callback(RequestWrapper wrapper, PagingRequestHelper helper) {
|
||||
mWrapper = wrapper;
|
||||
mHelper = helper;
|
||||
}
|
||||
/**
|
||||
* Call this method when the request succeeds and new data is fetched.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public final void recordSuccess() {
|
||||
if (mCalled.compareAndSet(false, true)) {
|
||||
mHelper.recordResult(mWrapper, null);
|
||||
} else {
|
||||
throw new IllegalStateException(
|
||||
"already called recordSuccess or recordFailure");
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Call this method with the failure message and the request can be retried via
|
||||
* {@link #retryAllFailed()}.
|
||||
*
|
||||
* @param throwable The error that occured while carrying out the request.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public final void recordFailure(@NonNull Throwable throwable) {
|
||||
//noinspection ConstantConditions
|
||||
if (throwable == null) {
|
||||
throw new IllegalArgumentException("You must provide a throwable describing"
|
||||
+ " the error to record the failure");
|
||||
}
|
||||
if (mCalled.compareAndSet(false, true)) {
|
||||
mHelper.recordResult(mWrapper, throwable);
|
||||
} else {
|
||||
throw new IllegalStateException(
|
||||
"already called recordSuccess or recordFailure");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Data class that holds the information about the current status of the ongoing requests
|
||||
* using this helper.
|
||||
*/
|
||||
public static final class StatusReport {
|
||||
/**
|
||||
* Status of the latest request that were submitted with {@link RequestType#INITIAL}.
|
||||
*/
|
||||
@NonNull
|
||||
public final Status initial;
|
||||
/**
|
||||
* Status of the latest request that were submitted with {@link RequestType#BEFORE}.
|
||||
*/
|
||||
@NonNull
|
||||
public final Status before;
|
||||
/**
|
||||
* Status of the latest request that were submitted with {@link RequestType#AFTER}.
|
||||
*/
|
||||
@NonNull
|
||||
public final Status after;
|
||||
@NonNull
|
||||
private final Throwable[] mErrors;
|
||||
StatusReport(@NonNull Status initial, @NonNull Status before, @NonNull Status after,
|
||||
@NonNull Throwable[] errors) {
|
||||
this.initial = initial;
|
||||
this.before = before;
|
||||
this.after = after;
|
||||
this.mErrors = errors;
|
||||
}
|
||||
/**
|
||||
* Convenience method to check if there are any running requests.
|
||||
*
|
||||
* @return True if there are any running requests, false otherwise.
|
||||
*/
|
||||
public boolean hasRunning() {
|
||||
return initial == Status.RUNNING
|
||||
|| before == Status.RUNNING
|
||||
|| after == Status.RUNNING;
|
||||
}
|
||||
/**
|
||||
* Convenience method to check if there are any requests that resulted in an error.
|
||||
*
|
||||
* @return True if there are any requests that finished with error, false otherwise.
|
||||
*/
|
||||
public boolean hasError() {
|
||||
return initial == Status.FAILED
|
||||
|| before == Status.FAILED
|
||||
|| after == Status.FAILED;
|
||||
}
|
||||
/**
|
||||
* Returns the error for the given request type.
|
||||
*
|
||||
* @param type The request type for which the error should be returned.
|
||||
* @return The {@link Throwable} returned by the failing request with the given type or
|
||||
* {@code null} if the request for the given type did not fail.
|
||||
*/
|
||||
@Nullable
|
||||
public Throwable getErrorFor(@NonNull RequestType type) {
|
||||
return mErrors[type.ordinal()];
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
return "StatusReport{"
|
||||
+ "initial=" + initial
|
||||
+ ", before=" + before
|
||||
+ ", after=" + after
|
||||
+ ", mErrors=" + Arrays.toString(mErrors)
|
||||
+ '}';
|
||||
}
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
StatusReport that = (StatusReport) o;
|
||||
if (initial != that.initial) return false;
|
||||
if (before != that.before) return false;
|
||||
if (after != that.after) return false;
|
||||
// Probably incorrect - comparing Object[] arrays with Arrays.equals
|
||||
return Arrays.equals(mErrors, that.mErrors);
|
||||
}
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = initial.hashCode();
|
||||
result = 31 * result + before.hashCode();
|
||||
result = 31 * result + after.hashCode();
|
||||
result = 31 * result + Arrays.hashCode(mErrors);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Listener interface to get notified by request status changes.
|
||||
*/
|
||||
public interface Listener {
|
||||
/**
|
||||
* Called when the status for any of the requests has changed.
|
||||
*
|
||||
* @param report The current status report that has all the information about the requests.
|
||||
*/
|
||||
void onStatusChange(@NonNull StatusReport report);
|
||||
}
|
||||
/**
|
||||
* Represents the status of a Request for each {@link RequestType}.
|
||||
*/
|
||||
public enum Status {
|
||||
/**
|
||||
* There is current a running request.
|
||||
*/
|
||||
RUNNING,
|
||||
/**
|
||||
* The last request has succeeded or no such requests have ever been run.
|
||||
*/
|
||||
SUCCESS,
|
||||
/**
|
||||
* The last request has failed.
|
||||
*/
|
||||
FAILED
|
||||
}
|
||||
/**
|
||||
* Available request types.
|
||||
*/
|
||||
public enum RequestType {
|
||||
/**
|
||||
* Corresponds to an initial request made to a {@link DataSource} or the empty state for
|
||||
* a {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}.
|
||||
*/
|
||||
INITIAL,
|
||||
/**
|
||||
* Corresponds to the {@code loadBefore} calls in {@link DataSource} or
|
||||
* {@code onItemAtFrontLoaded} in
|
||||
* {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}.
|
||||
*/
|
||||
BEFORE,
|
||||
/**
|
||||
* Corresponds to the {@code loadAfter} calls in {@link DataSource} or
|
||||
* {@code onItemAtEndLoaded} in
|
||||
* {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}.
|
||||
*/
|
||||
AFTER
|
||||
}
|
||||
class RequestQueue {
|
||||
@NonNull
|
||||
final RequestType mRequestType;
|
||||
@Nullable
|
||||
RequestWrapper mFailed;
|
||||
@Nullable
|
||||
Request mRunning;
|
||||
@Nullable
|
||||
Throwable mLastError;
|
||||
@NonNull
|
||||
Status mStatus = Status.SUCCESS;
|
||||
RequestQueue(@NonNull RequestType requestType) {
|
||||
mRequestType = requestType;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,7 +25,8 @@ import androidx.annotation.AttrRes;
|
|||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.ColorRes;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import android.util.TypedValue;
|
||||
import android.widget.ImageView;
|
||||
|
@ -37,12 +38,12 @@ import android.widget.ImageView;
|
|||
public class ThemeUtils {
|
||||
public static final String APP_THEME_DEFAULT = ThemeUtils.THEME_NIGHT;
|
||||
|
||||
public static final String THEME_NIGHT = "night";
|
||||
public static final String THEME_DAY = "day";
|
||||
public static final String THEME_BLACK = "black";
|
||||
public static final String THEME_AUTO = "auto";
|
||||
private static final String THEME_NIGHT = "night";
|
||||
private static final String THEME_DAY = "day";
|
||||
private static final String THEME_BLACK = "black";
|
||||
private static final String THEME_AUTO = "auto";
|
||||
|
||||
public static Drawable getDrawable(Context context, @AttrRes int attribute,
|
||||
public static Drawable getDrawable(@NonNull Context context, @AttrRes int attribute,
|
||||
@DrawableRes int fallbackDrawable) {
|
||||
TypedValue value = new TypedValue();
|
||||
@DrawableRes int resourceId;
|
||||
|
@ -51,10 +52,10 @@ public class ThemeUtils {
|
|||
} else {
|
||||
resourceId = fallbackDrawable;
|
||||
}
|
||||
return ContextCompat.getDrawable(context, resourceId);
|
||||
return context.getDrawable(resourceId);
|
||||
}
|
||||
|
||||
public static @DrawableRes int getDrawableId(Context context, @AttrRes int attribute,
|
||||
public static @DrawableRes int getDrawableId(@NonNull Context context, @AttrRes int attribute,
|
||||
@DrawableRes int fallbackDrawableId) {
|
||||
TypedValue value = new TypedValue();
|
||||
if (context.getTheme().resolveAttribute(attribute, value, true)) {
|
||||
|
@ -64,7 +65,7 @@ public class ThemeUtils {
|
|||
}
|
||||
}
|
||||
|
||||
public static @ColorInt int getColor(Context context, @AttrRes int attribute) {
|
||||
public static @ColorInt int getColor(@NonNull Context context, @AttrRes int attribute) {
|
||||
TypedValue value = new TypedValue();
|
||||
if (context.getTheme().resolveAttribute(attribute, value, true)) {
|
||||
return value.data;
|
||||
|
@ -73,13 +74,13 @@ public class ThemeUtils {
|
|||
}
|
||||
}
|
||||
|
||||
public static @ColorRes int getColorId(Context context, @AttrRes int attribute) {
|
||||
public static @ColorRes int getColorId(@NonNull Context context, @AttrRes int attribute) {
|
||||
TypedValue value = new TypedValue();
|
||||
context.getTheme().resolveAttribute(attribute, value, true);
|
||||
return value.resourceId;
|
||||
}
|
||||
|
||||
public static @ColorInt int getColorById(Context context, String name) {
|
||||
public static @ColorInt int getColorById(@NonNull Context context, String name) {
|
||||
return getColor(context,
|
||||
ResourcesUtils.getResourceIdentifier(context, "attr", name));
|
||||
}
|
||||
|
@ -88,6 +89,16 @@ public class ThemeUtils {
|
|||
view.setColorFilter(getColor(view.getContext(), attribute), PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
|
||||
/** this can be replaced with drawableTint in xml once minSdkVersion >= 23 */
|
||||
public static @Nullable Drawable getTintedDrawable(@NonNull Context context, @DrawableRes int drawableId, @AttrRes int colorAttr) {
|
||||
Drawable drawable = context.getDrawable(drawableId);
|
||||
if(drawable == null) {
|
||||
return null;
|
||||
}
|
||||
setDrawableTint(context, drawable, colorAttr);
|
||||
return drawable;
|
||||
}
|
||||
|
||||
public static void setDrawableTint(Context context, Drawable drawable, @AttrRes int attribute) {
|
||||
drawable.setColorFilter(getColor(context, attribute), PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package com.keylesspalace.tusky.util
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
|
||||
private fun getErrorMessage(report: PagingRequestHelper.StatusReport): String {
|
||||
return PagingRequestHelper.RequestType.values().mapNotNull {
|
||||
report.getErrorFor(it)?.message
|
||||
}.first()
|
||||
}
|
||||
|
||||
fun PagingRequestHelper.createStatusLiveData(): LiveData<NetworkState> {
|
||||
val liveData = MutableLiveData<NetworkState>()
|
||||
addListener { report ->
|
||||
when {
|
||||
report.hasRunning() -> liveData.postValue(NetworkState.LOADING)
|
||||
report.hasError() -> liveData.postValue(
|
||||
NetworkState.error(getErrorMessage(report)))
|
||||
else -> liveData.postValue(NetworkState.LOADED)
|
||||
}
|
||||
}
|
||||
return liveData
|
||||
}
|
5
app/src/main/res/color/tab_icon_color.xml
Normal file
5
app/src/main/res/color/tab_icon_color.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_selected="false" android:color="?attr/toolbar_icon_tint"/>
|
||||
<item android:state_selected="true" android:color="?attr/tab_icon_selected_tint"/>
|
||||
</selector>
|
6
app/src/main/res/drawable/avatar_border.xml
Normal file
6
app/src/main/res/drawable/avatar_border.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="?attr/window_background" />
|
||||
<corners android:radius="7dp"/>
|
||||
<size android:height="52dp" android:width="52dp"/>
|
||||
</shape>
|
9
app/src/main/res/drawable/ic_drag_indicator_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_drag_indicator_24dp.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?android:attr/textColorPrimary"
|
||||
android:pathData="M11,18c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2 0.9,-2 2,-2 2,0.9 2,2zM9,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM9,4c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM15,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM15,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM15,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_plus_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_plus_24dp.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />
|
||||
</vector>
|
|
@ -13,19 +13,19 @@
|
|||
android:background="?attr/window_background">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipe_refresh_layout"
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
|
@ -33,7 +33,6 @@
|
|||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
<com.keylesspalace.tusky.view.BackgroundMessageView
|
||||
android:id="@+id/statusView"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
@ -5,14 +5,14 @@
|
|||
android:background="?attr/tab_page_margin_drawable">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipe_refresh_layout"
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
android:layout_width="640dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:background="?attr/window_background">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical" />
|
||||
|
|
|
@ -36,33 +36,12 @@
|
|||
android:background="?android:colorBackground"
|
||||
android:elevation="@dimen/actionbar_elevation"
|
||||
app:tabGravity="fill"
|
||||
app:tabMaxWidth="0dp"
|
||||
app:tabIconTint="@color/tab_icon_color"
|
||||
app:tabMode="fixed"
|
||||
app:tabPaddingEnd="1dp"
|
||||
app:tabPaddingStart="1dp"
|
||||
app:tabPaddingTop="4dp"
|
||||
app:tabUnboundedRipple="false">
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/title_home" />
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/title_notifications" />
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/title_public_local" />
|
||||
|
||||
<com.google.android.material.tabs.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/title_public_federated" />
|
||||
|
||||
</com.google.android.material.tabs.TabLayout>
|
||||
app:tabUnboundedRipple="false" />
|
||||
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
android:id="@+id/pager"
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
android:visibility="invisible" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
|
|
80
app/src/main/res/layout/activity_tab_preference.xml
Normal file
80
app/src/main/res/layout/activity_tab_preference.xml
Normal file
|
@ -0,0 +1,80 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/toolbar_basic" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/currentTabsRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
app:layout_constraintTop_toBottomOf="@id/appbar" />
|
||||
|
||||
<View
|
||||
android:id="@+id/scrim"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/scrimBackground"
|
||||
android:visibility="invisible"
|
||||
app:layout_behavior="@string/fab_transformation_scrim_behavior" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/actionButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:src="@drawable/ic_plus_24dp" />
|
||||
|
||||
<com.google.android.material.transformation.TransformationChildCard
|
||||
android:id="@+id/sheet"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:visibility="invisible"
|
||||
app:cardBackgroundColor="?attr/colorSurface"
|
||||
app:cardElevation="2dp"
|
||||
app:layout_behavior="@string/fab_transformation_sheet_behavior">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="240dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/addTabRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:overScrollMode="never" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/maxTabsInfo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="maximum of 5 tabs reached"
|
||||
android:padding="8dp"
|
||||
android:lineSpacingMultiplier="1.1" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:drawableStart="@drawable/ic_plus_24dp"
|
||||
android:drawablePadding="12dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical"
|
||||
android:lines="1"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:text="@string/action_add_tab"
|
||||
android:textColor="?attr/colorOnPrimary"
|
||||
android:textSize="?attr/status_text_large" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.transformation.TransformationChildCard>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -6,19 +6,19 @@
|
|||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipe_refresh_layout"
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/swipe_refresh_layout"
|
||||
android:id="@+id/swipeRefreshLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="top">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical" />
|
||||
|
|
391
app/src/main/res/layout/item_conversation.xml
Normal file
391
app/src/main/res/layout/item_conversation.xml
Normal file
|
@ -0,0 +1,391 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:sparkbutton="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/status_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="14dp">
|
||||
|
||||
<androidx.emoji.widget.EmojiTextView
|
||||
android:id="@+id/conversation_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="2dp"
|
||||
android:layout_marginTop="@dimen/status_reblogged_bar_padding_top"
|
||||
android:gravity="center_vertical"
|
||||
android:lineSpacingMultiplier="1.1"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
android:textStyle="normal|bold"
|
||||
app:layout_constraintLeft_toRightOf="parent"
|
||||
app:layout_constraintRight_toLeftOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:ignore="RtlSymmetry"
|
||||
tools:text="ConnyDuck boosted"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.keylesspalace.tusky.view.RoundedImageView
|
||||
android:id="@+id/status_avatar_2"
|
||||
android:layout_width="52dp"
|
||||
android:layout_height="52dp"
|
||||
android:layout_marginTop="22dp"
|
||||
android:background="@drawable/avatar_border"
|
||||
android:contentDescription="@string/action_view_profile"
|
||||
android:padding="2dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/status_avatar_1"
|
||||
tools:src="@drawable/avatar_default" />
|
||||
|
||||
<com.keylesspalace.tusky.view.RoundedImageView
|
||||
android:id="@+id/status_avatar_1"
|
||||
android:layout_width="52dp"
|
||||
android:layout_height="52dp"
|
||||
android:layout_marginTop="22dp"
|
||||
android:background="@drawable/avatar_border"
|
||||
android:contentDescription="@string/action_view_profile"
|
||||
android:padding="2dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/status_avatar"
|
||||
tools:src="@drawable/avatar_default" />
|
||||
|
||||
<com.keylesspalace.tusky.view.RoundedImageView
|
||||
android:id="@+id/status_avatar"
|
||||
android:layout_width="52dp"
|
||||
android:layout_height="52dp"
|
||||
android:layout_marginTop="14dp"
|
||||
android:background="@drawable/avatar_border"
|
||||
android:contentDescription="@string/action_view_profile"
|
||||
android:padding="2dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/conversation_name"
|
||||
tools:src="@drawable/avatar_default" />
|
||||
|
||||
<com.keylesspalace.tusky.view.RoundedImageView
|
||||
android:id="@+id/status_avatar_reblog"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:contentDescription="@null"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/status_avatar"
|
||||
app:layout_constraintEnd_toEndOf="@id/status_avatar"
|
||||
tools:src="@color/accent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.emoji.widget.EmojiTextView
|
||||
android:id="@+id/status_display_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="14dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:paddingEnd="@dimen/status_display_name_padding_end"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
android:textStyle="normal|bold"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toStartOf="@id/status_timestamp_info"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toEndOf="@id/status_avatar"
|
||||
app:layout_constraintTop_toBottomOf="@id/conversation_name"
|
||||
tools:text="Ente r the void you foooooo" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/status_username"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
app:layout_constraintEnd_toStartOf="@id/status_timestamp_info"
|
||||
app:layout_constraintStart_toEndOf="@id/status_display_name"
|
||||
app:layout_constraintTop_toTopOf="@id/status_display_name"
|
||||
tools:text="\@Entenhausen@birbsarecooooooooooool.site" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/status_timestamp_info"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/status_display_name"
|
||||
tools:text="13:37" />
|
||||
|
||||
<androidx.emoji.widget.EmojiTextView
|
||||
android:id="@+id/status_content_warning_description"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:lineSpacingMultiplier="1.1"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||
app:layout_constraintTop_toBottomOf="@id/status_display_name"
|
||||
tools:text="content warning which is very long and it doesn't fit"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ToggleButton
|
||||
android:id="@+id/status_content_warning_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:background="?attr/content_warning_button"
|
||||
android:minWidth="150dp"
|
||||
android:minHeight="0dp"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingBottom="4dp"
|
||||
android:textAllCaps="true"
|
||||
android:textOff="@string/status_content_warning_show_more"
|
||||
android:textOn="@string/status_content_warning_show_less"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||
app:layout_constraintTop_toBottomOf="@id/status_content_warning_description"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.emoji.widget.EmojiTextView
|
||||
android:id="@+id/status_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:focusable="true"
|
||||
android:lineSpacingMultiplier="1.1"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@id/status_content_warning_button"
|
||||
app:layout_constraintTop_toBottomOf="@id/status_content_warning_button"
|
||||
tools:text="This is a status" />
|
||||
|
||||
<ToggleButton
|
||||
android:id="@+id/button_toggle_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:background="?attr/content_warning_button"
|
||||
android:minWidth="150dp"
|
||||
android:minHeight="0dp"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingBottom="4dp"
|
||||
android:textAllCaps="true"
|
||||
android:textOff="@string/status_content_show_less"
|
||||
android:textOn="@string/status_content_show_more"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||
app:layout_constraintTop_toBottomOf="@id/status_content"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/status_media_preview_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/status_media_preview_margin_top"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||
app:layout_constraintTop_toBottomOf="@id/button_toggle_content"
|
||||
tools:visibility="gone">
|
||||
|
||||
<com.keylesspalace.tusky.view.MediaPreviewImageView
|
||||
android:id="@+id/status_media_preview_0"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/status_media_preview_height"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintEnd_toStartOf="@+id/status_media_preview_1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<com.keylesspalace.tusky.view.MediaPreviewImageView
|
||||
android:id="@+id/status_media_preview_1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/status_media_preview_height"
|
||||
android:layout_marginStart="4dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/status_media_preview_0"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
|
||||
<com.keylesspalace.tusky.view.MediaPreviewImageView
|
||||
android:id="@+id/status_media_preview_2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/status_media_preview_height"
|
||||
android:layout_marginTop="4dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintEnd_toStartOf="@+id/status_media_preview_3"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/status_media_preview_0"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<com.keylesspalace.tusky.view.MediaPreviewImageView
|
||||
android:id="@+id/status_media_preview_3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/status_media_preview_height"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/status_media_preview_2"
|
||||
app:layout_constraintTop_toBottomOf="@+id/status_media_preview_1"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/status_media_overlay_0"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:scaleType="center"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/status_media_preview_0"
|
||||
app:layout_constraintEnd_toEndOf="@+id/status_media_preview_0"
|
||||
app:layout_constraintStart_toStartOf="@+id/status_media_preview_0"
|
||||
app:layout_constraintTop_toTopOf="@+id/status_media_preview_0"
|
||||
app:srcCompat="?attr/play_indicator_drawable"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/status_media_overlay_1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:scaleType="center"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/status_media_preview_1"
|
||||
app:layout_constraintEnd_toEndOf="@+id/status_media_preview_1"
|
||||
app:layout_constraintStart_toStartOf="@+id/status_media_preview_1"
|
||||
app:layout_constraintTop_toTopOf="@+id/status_media_preview_1"
|
||||
app:srcCompat="?attr/play_indicator_drawable"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/status_media_overlay_2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:scaleType="center"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/status_media_preview_2"
|
||||
app:layout_constraintEnd_toEndOf="@+id/status_media_preview_2"
|
||||
app:layout_constraintStart_toStartOf="@+id/status_media_preview_2"
|
||||
app:layout_constraintTop_toTopOf="@+id/status_media_preview_2"
|
||||
app:srcCompat="?attr/play_indicator_drawable"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/status_media_overlay_3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:scaleType="center"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/status_media_preview_3"
|
||||
app:layout_constraintEnd_toEndOf="@+id/status_media_preview_3"
|
||||
app:layout_constraintStart_toStartOf="@+id/status_media_preview_3"
|
||||
app:layout_constraintTop_toTopOf="@+id/status_media_preview_3"
|
||||
app:srcCompat="?attr/play_indicator_drawable"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/status_sensitive_media_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:alpha="0.7"
|
||||
android:contentDescription="@null"
|
||||
android:padding="@dimen/status_sensitive_media_button_padding"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintLeft_toLeftOf="@+id/status_media_preview_container"
|
||||
app:layout_constraintTop_toTopOf="@+id/status_media_preview_container"
|
||||
app:srcCompat="@drawable/ic_eye_24dp" />
|
||||
|
||||
<androidx.emoji.widget.EmojiTextView
|
||||
android:id="@+id/status_sensitive_media_warning"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="?attr/sensitive_media_warning_background_color"
|
||||
android:gravity="center"
|
||||
android:lineSpacingMultiplier="1.2"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<!--TODO: Check if this needs emoji support-->
|
||||
<androidx.emoji.widget.EmojiTextView
|
||||
android:id="@+id/status_media_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:drawablePadding="4dp"
|
||||
android:gravity="center_vertical"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/status_reply"
|
||||
style="?attr/image_button_style"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:contentDescription="@string/action_reply"
|
||||
android:padding="4dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/status_favourite"
|
||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||
app:layout_constraintTop_toBottomOf="@id/status_media_preview_container"
|
||||
app:srcCompat="@drawable/ic_reply_24dp" />
|
||||
|
||||
<at.connyduck.sparkbutton.SparkButton
|
||||
android:id="@+id/status_favourite"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:clipToPadding="false"
|
||||
android:contentDescription="@string/action_favourite"
|
||||
android:padding="4dp"
|
||||
app:layout_constraintEnd_toStartOf="@id/status_more"
|
||||
app:layout_constraintStart_toEndOf="@id/status_reply"
|
||||
app:layout_constraintTop_toTopOf="@id/status_reply"
|
||||
sparkbutton:activeImage="?attr/status_favourite_active_drawable"
|
||||
sparkbutton:iconSize="28dp"
|
||||
sparkbutton:inactiveImage="?attr/status_favourite_inactive_drawable"
|
||||
sparkbutton:primaryColor="@color/tusky_orange"
|
||||
sparkbutton:secondaryColor="@color/tusky_orange_light" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/status_more"
|
||||
style="?attr/image_button_style"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:contentDescription="@string/action_more"
|
||||
android:padding="4dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/status_reply"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/status_favourite"
|
||||
app:layout_constraintTop_toTopOf="@id/status_reply"
|
||||
app:srcCompat="@drawable/ic_more_horiz_24dp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
23
app/src/main/res/layout/item_network_state.xml
Normal file
23
app/src/main/res/layout/item_network_state.xml
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:padding="8dp">
|
||||
<TextView
|
||||
android:id="@+id/errorMsg"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
<Button
|
||||
android:id="@+id/retryButton"
|
||||
style="@style/TuskyButton.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/action_retry"/>
|
||||
</LinearLayout>
|
|
@ -13,7 +13,7 @@
|
|||
android:textSize="?attr/status_text_medium" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
|
|
28
app/src/main/res/layout/item_tab_preference.xml
Normal file
28
app/src/main/res/layout/item_tab_preference.xml
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:colorBackground"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp">
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_gravity="end"
|
||||
android:src="@drawable/ic_drag_indicator_24dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:textSize="?attr/status_text_large"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:drawableStart="@drawable/ic_home_24dp"
|
||||
android:layout_width="0dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:drawablePadding="12dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
17
app/src/main/res/layout/item_tab_preference_small.xml
Normal file
17
app/src/main/res/layout/item_tab_preference_small.xml
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="match_parent"
|
||||
android:drawableStart="@drawable/ic_home_24dp"
|
||||
android:layout_height="48dp"
|
||||
android:gravity="center_vertical"
|
||||
android:drawablePadding="12dp"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:lines="1"
|
||||
android:ellipsize="end"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="?attr/status_text_large" />
|
||||
|
||||
|
|
@ -30,4 +30,6 @@
|
|||
<item name="wrap_content" format="integer" type="dimen">-2</item>
|
||||
|
||||
<dimen name="preference_icon_size">20dp</dimen>
|
||||
|
||||
<dimen name="selected_drag_item_elevation">12dp</dimen>
|
||||
</resources>
|
||||
|
|
|
@ -27,6 +27,8 @@
|
|||
<string name="title_notifications">Notifications</string>
|
||||
<string name="title_public_local">Local</string>
|
||||
<string name="title_public_federated">Federated</string>
|
||||
<string name="title_direct_messages">Direct Messages</string>
|
||||
<string name="title_tab_preferences">Tabs</string>
|
||||
<string name="title_view_thread">Toot</string>
|
||||
<string name="title_tag">#%s</string>
|
||||
<string name="title_statuses">Posts</string>
|
||||
|
@ -111,6 +113,7 @@
|
|||
<string name="action_toggle_visibility">Toot visibility</string>
|
||||
<string name="action_content_warning">Content warning</string>
|
||||
<string name="action_emoji_keyboard">Emoji keyboard</string>
|
||||
<string name="action_add_tab">Add Tab</string>
|
||||
|
||||
<string name="download_image">Downloading %1$s</string>
|
||||
|
||||
|
@ -378,4 +381,9 @@
|
|||
<string name="title_reblogged_by">Boosted by</string>
|
||||
<string name="title_favourited_by">Favourited by</string>
|
||||
|
||||
<string name="conversation_1_recipients">%1$s</string>
|
||||
<string name="conversation_2_recipients">%1$s and %2$s</string>
|
||||
<string name="conversation_more_recipients">%1$s, %2$s and %3$d more</string>
|
||||
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -6,6 +6,10 @@
|
|||
android:key="notificationPreference"
|
||||
android:title="@string/pref_title_edit_notification_settings" />
|
||||
|
||||
<Preference
|
||||
android:key="tabPreference"
|
||||
android:title="@string/title_tab_preferences" />
|
||||
|
||||
<Preference
|
||||
android:key="mutedUsersPreference"
|
||||
android:title="@string/action_view_mutes" />
|
||||
|
|
|
@ -196,7 +196,7 @@ class BottomSheetActivityTest {
|
|||
fun search_inIdealConditions_returnsRequestedResults_forStatus() {
|
||||
activity.viewUrl(statusQuery)
|
||||
statusCallback.invokeCallback()
|
||||
Assert.assertEquals(status, activity.status)
|
||||
Assert.assertEquals(status.id, activity.statusId)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -250,7 +250,7 @@ class BottomSheetActivityTest {
|
|||
|
||||
// ensure that the result of the status search was recorded
|
||||
// and the account search wasn't
|
||||
Assert.assertEquals(status, activity.status)
|
||||
Assert.assertEquals(status.id, activity.statusId)
|
||||
Assert.assertEquals(null, activity.accountId)
|
||||
}
|
||||
|
||||
|
@ -288,7 +288,7 @@ class BottomSheetActivityTest {
|
|||
|
||||
class FakeBottomSheetActivity(api: MastodonApi) : BottomSheetActivity() {
|
||||
|
||||
var status: Status? = null
|
||||
var statusId: String? = null
|
||||
var accountId: String? = null
|
||||
var link: String? = null
|
||||
|
||||
|
@ -307,8 +307,8 @@ class BottomSheetActivityTest {
|
|||
this.accountId = id
|
||||
}
|
||||
|
||||
override fun viewThread(status: Status) {
|
||||
this.status = status
|
||||
override fun viewThread(statusId: String, url: String?) {
|
||||
this.statusId = statusId
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue