Polls part 1 - displaying in timelines and voting (#1200)
* add entity classes * change data models and add database migration * add polls to StatusViewData * show poll results * add methods for vote handling * add voting interface * enable voting in TimelineFragment * update polls immediately * enable custom emojis for poll options * enable voting from search fragment * add voting layout to detailed statuses * fix tests * enable voting in ViewThreadFragment * enable voting in ConversationsFragment * small refactor for StatusBaseViewHolder
This commit is contained in:
parent
82d547caf8
commit
fd7471f2ab
36 changed files with 1637 additions and 68 deletions
674
app/schemas/com.keylesspalace.tusky.db.AppDatabase/15.json
Normal file
674
app/schemas/com.keylesspalace.tusky.db.AppDatabase/15.json
Normal file
|
@ -0,0 +1,674 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 15,
|
||||||
|
"identityHash": "6a01315ce9f7d402cb61e611140e3c0a",
|
||||||
|
"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, `notificationsFilter` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "domain",
|
||||||
|
"columnName": "domain",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessToken",
|
||||||
|
"columnName": "accessToken",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isActive",
|
||||||
|
"columnName": "isActive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "profilePictureUrl",
|
||||||
|
"columnName": "profilePictureUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsEnabled",
|
||||||
|
"columnName": "notificationsEnabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsMentioned",
|
||||||
|
"columnName": "notificationsMentioned",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFollowed",
|
||||||
|
"columnName": "notificationsFollowed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsReblogged",
|
||||||
|
"columnName": "notificationsReblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFavorited",
|
||||||
|
"columnName": "notificationsFavorited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "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
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFilter",
|
||||||
|
"columnName": "notificationsFilter",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_AccountEntity_domain_accountId",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"domain",
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX `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, `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, `poll` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timelineUserId",
|
||||||
|
"columnName": "timelineUserId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "authorServerId",
|
||||||
|
"columnName": "authorServerId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToId",
|
||||||
|
"columnName": "inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToAccountId",
|
||||||
|
"columnName": "inReplyToAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "createdAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogsCount",
|
||||||
|
"columnName": "reblogsCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favouritesCount",
|
||||||
|
"columnName": "favouritesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogged",
|
||||||
|
"columnName": "reblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favourited",
|
||||||
|
"columnName": "favourited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensitive",
|
||||||
|
"columnName": "sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "spoilerText",
|
||||||
|
"columnName": "spoilerText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mentions",
|
||||||
|
"columnName": "mentions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "application",
|
||||||
|
"columnName": "application",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogServerId",
|
||||||
|
"columnName": "reblogServerId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogAccountId",
|
||||||
|
"columnName": "reblogAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "poll",
|
||||||
|
"columnName": "poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "TimelineAccountEntity",
|
||||||
|
"onDelete": "NO ACTION",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "TimelineAccountEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timelineUserId",
|
||||||
|
"columnName": "timelineUserId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "localUsername",
|
||||||
|
"columnName": "localUsername",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatar",
|
||||||
|
"columnName": "avatar",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accounts",
|
||||||
|
"columnName": "accounts",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unread",
|
||||||
|
"columnName": "unread",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.id",
|
||||||
|
"columnName": "s_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.url",
|
||||||
|
"columnName": "s_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.inReplyToId",
|
||||||
|
"columnName": "s_inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.inReplyToAccountId",
|
||||||
|
"columnName": "s_inReplyToAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.account",
|
||||||
|
"columnName": "s_account",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.content",
|
||||||
|
"columnName": "s_content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.createdAt",
|
||||||
|
"columnName": "s_createdAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.emojis",
|
||||||
|
"columnName": "s_emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.favouritesCount",
|
||||||
|
"columnName": "s_favouritesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.favourited",
|
||||||
|
"columnName": "s_favourited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.sensitive",
|
||||||
|
"columnName": "s_sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.spoilerText",
|
||||||
|
"columnName": "s_spoilerText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.attachments",
|
||||||
|
"columnName": "s_attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.mentions",
|
||||||
|
"columnName": "s_mentions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.showingHiddenContent",
|
||||||
|
"columnName": "s_showingHiddenContent",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.expanded",
|
||||||
|
"columnName": "s_expanded",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.collapsible",
|
||||||
|
"columnName": "s_collapsible",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.collapsed",
|
||||||
|
"columnName": "s_collapsed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.poll",
|
||||||
|
"columnName": "s_poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id",
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, \"6a01315ce9f7d402cb61e611140e3c0a\")"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -74,7 +74,7 @@ public class TuskyApplication extends Application implements HasActivityInjector
|
||||||
AppDatabase.MIGRATION_5_6, AppDatabase.MIGRATION_6_7, AppDatabase.MIGRATION_7_8,
|
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, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13,
|
AppDatabase.MIGRATION_11_12, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13,
|
||||||
AppDatabase.MIGRATION_13_14)
|
AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15)
|
||||||
.build();
|
.build();
|
||||||
accountManager = new AccountManager(appDatabase);
|
accountManager = new AccountManager(appDatabase);
|
||||||
serviceLocator = new ServiceLocator() {
|
serviceLocator = new ServiceLocator() {
|
||||||
|
|
|
@ -143,6 +143,7 @@ public class SearchResultsAdapter extends RecyclerView.Adapter {
|
||||||
|
|
||||||
public void updateStatusAtPosition(StatusViewData.Concrete status, int position) {
|
public void updateStatusAtPosition(StatusViewData.Concrete status, int position) {
|
||||||
concreteStatusList.set(position - accountList.size(), status);
|
concreteStatusList.set(position - accountList.size(), status);
|
||||||
|
notifyItemChanged(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeStatusAtPosition(int position) {
|
public void removeStatusAtPosition(int position) {
|
||||||
|
|
|
@ -7,8 +7,11 @@ import android.text.Spanned;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Button;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
import android.widget.RadioButton;
|
||||||
|
import android.widget.RadioGroup;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.ToggleButton;
|
import android.widget.ToggleButton;
|
||||||
|
|
||||||
|
@ -18,6 +21,8 @@ import com.keylesspalace.tusky.entity.Attachment;
|
||||||
import com.keylesspalace.tusky.entity.Attachment.Focus;
|
import com.keylesspalace.tusky.entity.Attachment.Focus;
|
||||||
import com.keylesspalace.tusky.entity.Attachment.MetaData;
|
import com.keylesspalace.tusky.entity.Attachment.MetaData;
|
||||||
import com.keylesspalace.tusky.entity.Emoji;
|
import com.keylesspalace.tusky.entity.Emoji;
|
||||||
|
import com.keylesspalace.tusky.entity.Poll;
|
||||||
|
import com.keylesspalace.tusky.entity.PollOption;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||||
|
@ -31,6 +36,7 @@ import com.mikepenz.iconics.utils.Utils;
|
||||||
|
|
||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
@ -70,6 +76,12 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
public TextView content;
|
public TextView content;
|
||||||
public TextView contentWarningDescription;
|
public TextView contentWarningDescription;
|
||||||
|
|
||||||
|
private TextView[] pollResults;
|
||||||
|
private TextView pollDescription;
|
||||||
|
private RadioGroup pollRadioGroup;
|
||||||
|
private RadioButton[] pollRadioOptions;
|
||||||
|
private Button pollButton;
|
||||||
|
|
||||||
private boolean useAbsoluteTime;
|
private boolean useAbsoluteTime;
|
||||||
private SimpleDateFormat shortSdf;
|
private SimpleDateFormat shortSdf;
|
||||||
private SimpleDateFormat longSdf;
|
private SimpleDateFormat longSdf;
|
||||||
|
@ -109,6 +121,25 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
contentWarningButton = itemView.findViewById(R.id.status_content_warning_button);
|
contentWarningButton = itemView.findViewById(R.id.status_content_warning_button);
|
||||||
avatarInset = itemView.findViewById(R.id.status_avatar_inset);
|
avatarInset = itemView.findViewById(R.id.status_avatar_inset);
|
||||||
|
|
||||||
|
pollResults = new TextView[] {
|
||||||
|
itemView.findViewById(R.id.status_poll_option_result_0),
|
||||||
|
itemView.findViewById(R.id.status_poll_option_result_1),
|
||||||
|
itemView.findViewById(R.id.status_poll_option_result_2),
|
||||||
|
itemView.findViewById(R.id.status_poll_option_result_3)
|
||||||
|
};
|
||||||
|
|
||||||
|
pollDescription = itemView.findViewById(R.id.status_poll_description);
|
||||||
|
|
||||||
|
pollRadioGroup = itemView.findViewById(R.id.status_poll_radio_group);
|
||||||
|
pollRadioOptions = new RadioButton[] {
|
||||||
|
pollRadioGroup.findViewById(R.id.status_poll_radio_button_0),
|
||||||
|
pollRadioGroup.findViewById(R.id.status_poll_radio_button_1),
|
||||||
|
pollRadioGroup.findViewById(R.id.status_poll_radio_button_2),
|
||||||
|
pollRadioGroup.findViewById(R.id.status_poll_radio_button_3)
|
||||||
|
};
|
||||||
|
|
||||||
|
pollButton = itemView.findViewById(R.id.status_poll_button);
|
||||||
|
|
||||||
this.useAbsoluteTime = useAbsoluteTime;
|
this.useAbsoluteTime = useAbsoluteTime;
|
||||||
shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
|
shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
|
||||||
longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault());
|
longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault());
|
||||||
|
@ -218,10 +249,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
private String getAbsoluteTime(@Nullable Date createdAt) {
|
private String getAbsoluteTime(@Nullable Date createdAt) {
|
||||||
String time;
|
String time;
|
||||||
if (createdAt != null) {
|
if (createdAt != null) {
|
||||||
if (System.currentTimeMillis() - createdAt.getTime() > 86400000L) {
|
if (android.text.format.DateUtils.isToday(createdAt.getTime())) {
|
||||||
time = longSdf.format(createdAt);
|
|
||||||
} else {
|
|
||||||
time = shortSdf.format(createdAt);
|
time = shortSdf.format(createdAt);
|
||||||
|
} else {
|
||||||
|
time = longSdf.format(createdAt);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
time = "??:??:??";
|
time = "??:??:??";
|
||||||
|
@ -588,6 +619,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getStatusEmojis(), listener);
|
setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getStatusEmojis(), listener);
|
||||||
|
|
||||||
setContentDescription(status);
|
setContentDescription(status);
|
||||||
|
|
||||||
|
setupPoll(status.getPoll(),status.getStatusEmojis(), listener);
|
||||||
|
|
||||||
// Workaround for RecyclerView 1.0.0 / androidx.core 1.0.0
|
// Workaround for RecyclerView 1.0.0 / androidx.core 1.0.0
|
||||||
// RecyclerView tries to set AccessibilityDelegateCompat to null
|
// RecyclerView tries to set AccessibilityDelegateCompat to null
|
||||||
// but ViewCompat code replaces is with the default one. RecyclerView never
|
// but ViewCompat code replaces is with the default one. RecyclerView never
|
||||||
|
@ -717,4 +751,124 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void setupPoll(Poll poll, List<Emoji> emojis, StatusActionListener listener) {
|
||||||
|
if(poll == null) {
|
||||||
|
for(TextView pollResult: pollResults) {
|
||||||
|
pollResult.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
pollDescription.setVisibility(View.GONE);
|
||||||
|
pollRadioGroup.setVisibility(View.GONE);
|
||||||
|
|
||||||
|
for(RadioButton radioButton: pollRadioOptions) {
|
||||||
|
radioButton.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
pollButton.setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
|
Context context = pollDescription.getContext();
|
||||||
|
List<PollOption> options = poll.getOptions();
|
||||||
|
|
||||||
|
if(poll.getExpired() || poll.getVoted()) {
|
||||||
|
// no voting possible
|
||||||
|
for(int i = 0; i < Status.MAX_POLL_OPTIONS; i++) {
|
||||||
|
if(i < options.size()) {
|
||||||
|
long percent = calculatePollPercent(options.get(i).getVotesCount(), poll.getVotesCount());
|
||||||
|
|
||||||
|
String pollOptionText = context.getString(R.string.poll_option_format, percent, options.get(i).getTitle());
|
||||||
|
pollResults[i].setText(CustomEmojiHelper.emojifyText(HtmlUtils.fromHtml(pollOptionText), emojis, pollResults[i]));
|
||||||
|
pollResults[i].setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
int level = (int) percent * 100;
|
||||||
|
|
||||||
|
pollResults[i].getBackground().setLevel(level);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
pollResults[i].setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pollRadioGroup.setVisibility(View.GONE);
|
||||||
|
|
||||||
|
for(RadioButton radioButton: pollRadioOptions) {
|
||||||
|
radioButton.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
pollButton.setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
|
// voting possible
|
||||||
|
|
||||||
|
for(TextView pollResult: pollResults) {
|
||||||
|
pollResult.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
pollRadioGroup.setVisibility(View.VISIBLE);
|
||||||
|
pollRadioGroup.clearCheck();
|
||||||
|
pollButton.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
for(int i = 0; i < Status.MAX_POLL_OPTIONS; i++) {
|
||||||
|
if(i < options.size()) {
|
||||||
|
pollRadioOptions[i].setText(CustomEmojiHelper.emojifyString(options.get(i).getTitle(), emojis, pollRadioOptions[i]));
|
||||||
|
pollRadioOptions[i].setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
pollRadioOptions[i].setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pollDescription.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
String votes = numberFormat.format(poll.getVotesCount());
|
||||||
|
String votesText = context.getResources().getQuantityString(R.plurals.poll_info_votes, poll.getVotesCount(), votes);
|
||||||
|
|
||||||
|
CharSequence pollDurationInfo;
|
||||||
|
if(poll.getExpired()) {
|
||||||
|
pollDurationInfo = context.getString(R.string.poll_info_closed);
|
||||||
|
} else {
|
||||||
|
if(useAbsoluteTime) {
|
||||||
|
pollDurationInfo = context.getString(R.string.poll_info_time_absolute, getAbsoluteTime(poll.getExpiresAt()));
|
||||||
|
} else {
|
||||||
|
String pollDuration = DateUtils.formatDuration(pollDescription.getContext(), poll.getExpiresAt().getTime(), System.currentTimeMillis());
|
||||||
|
pollDurationInfo = context.getString(R.string.poll_info_time_relative, pollDuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String pollInfo = pollDescription.getContext().getString(R.string.poll_info_format, votesText, pollDurationInfo);
|
||||||
|
|
||||||
|
pollDescription.setText(pollInfo);
|
||||||
|
|
||||||
|
pollButton.setOnClickListener(v -> {
|
||||||
|
|
||||||
|
int selectedRadioButtonIndex;
|
||||||
|
switch (pollRadioGroup.getCheckedRadioButtonId()) {
|
||||||
|
case R.id.status_poll_radio_button_0:
|
||||||
|
selectedRadioButtonIndex = 0;
|
||||||
|
break;
|
||||||
|
case R.id.status_poll_radio_button_1:
|
||||||
|
selectedRadioButtonIndex = 1;
|
||||||
|
break;
|
||||||
|
case R.id.status_poll_radio_button_2:
|
||||||
|
selectedRadioButtonIndex = 2;
|
||||||
|
break;
|
||||||
|
case R.id.status_poll_radio_button_3:
|
||||||
|
selectedRadioButtonIndex = 3;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
listener.onVoteInPoll(getAdapterPosition(), Collections.singletonList(selectedRadioButtonIndex));
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long calculatePollPercent(int votes, int totalVotes) {
|
||||||
|
if(votes == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Math.round(votes / (double) totalVotes * 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.keylesspalace.tusky.appstore
|
||||||
|
|
||||||
import com.keylesspalace.tusky.TabData
|
import com.keylesspalace.tusky.TabData
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
|
||||||
data class FavoriteEvent(val statusId: String, val favourite: Boolean) : Dispatchable
|
data class FavoriteEvent(val statusId: String, val favourite: Boolean) : Dispatchable
|
||||||
|
@ -13,4 +14,5 @@ data class StatusDeletedEvent(val statusId: String) : Dispatchable
|
||||||
data class StatusComposedEvent(val status: Status) : Dispatchable
|
data class StatusComposedEvent(val status: Status) : Dispatchable
|
||||||
data class ProfileEditedEvent(val newProfileData: Account) : 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
|
data class MainTabsChangedEvent(val newTabs: List<TabData>) : Dispatchable
|
||||||
|
data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable
|
|
@ -76,7 +76,8 @@ data class ConversationStatusEntity(
|
||||||
val showingHiddenContent: Boolean,
|
val showingHiddenContent: Boolean,
|
||||||
val expanded: Boolean,
|
val expanded: Boolean,
|
||||||
val collapsible: Boolean,
|
val collapsible: Boolean,
|
||||||
val collapsed: Boolean
|
val collapsed: Boolean,
|
||||||
|
val poll: Poll?
|
||||||
|
|
||||||
) {
|
) {
|
||||||
/** its necessary to override this because Spanned.equals does not work as expected */
|
/** its necessary to override this because Spanned.equals does not work as expected */
|
||||||
|
@ -104,6 +105,7 @@ data class ConversationStatusEntity(
|
||||||
if (expanded != other.expanded) return false
|
if (expanded != other.expanded) return false
|
||||||
if (collapsible != other.collapsible) return false
|
if (collapsible != other.collapsible) return false
|
||||||
if (collapsed != other.collapsed) return false
|
if (collapsed != other.collapsed) return false
|
||||||
|
if (poll != other.poll) return false
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -127,6 +129,7 @@ data class ConversationStatusEntity(
|
||||||
result = 31 * result + expanded.hashCode()
|
result = 31 * result + expanded.hashCode()
|
||||||
result = 31 * result + collapsible.hashCode()
|
result = 31 * result + collapsible.hashCode()
|
||||||
result = 31 * result + collapsed.hashCode()
|
result = 31 * result + collapsed.hashCode()
|
||||||
|
result = 31 * result + poll.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,7 +154,8 @@ data class ConversationStatusEntity(
|
||||||
attachments = attachments,
|
attachments = attachments,
|
||||||
mentions = mentions,
|
mentions = mentions,
|
||||||
application = null,
|
application = null,
|
||||||
pinned = false)
|
pinned = false,
|
||||||
|
poll = poll)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,7 +176,8 @@ fun Status.toEntity() =
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
!SmartLengthInputFilter.hasBadRatio(content, SmartLengthInputFilter.LENGTH_DEFAULT),
|
!SmartLengthInputFilter.hasBadRatio(content, SmartLengthInputFilter.LENGTH_DEFAULT),
|
||||||
true
|
true,
|
||||||
|
poll
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -102,6 +102,8 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
||||||
|
|
||||||
setAvatars(conversation.getAccounts());
|
setAvatars(conversation.getAccounts());
|
||||||
|
|
||||||
|
setupPoll(status.getPoll(), status.getEmojis(), listener);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setConversationName(List<ConversationAccountEntity> accounts) {
|
private void setConversationName(List<ConversationAccountEntity> accounts) {
|
||||||
|
|
|
@ -18,9 +18,11 @@ package com.keylesspalace.tusky.components.conversation
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.preference.PreferenceManager
|
import android.preference.PreferenceManager
|
||||||
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.lifecycle.ViewModelProviders
|
import androidx.lifecycle.ViewModelProviders
|
||||||
import androidx.paging.PagedList
|
import androidx.paging.PagedList
|
||||||
|
@ -34,11 +36,15 @@ import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
import com.keylesspalace.tusky.fragment.SFragment
|
import com.keylesspalace.tusky.fragment.SFragment
|
||||||
|
import com.keylesspalace.tusky.fragment.SearchFragment
|
||||||
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||||
import com.keylesspalace.tusky.util.NetworkState
|
import com.keylesspalace.tusky.util.NetworkState
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils
|
import com.keylesspalace.tusky.util.ThemeUtils
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
|
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider
|
||||||
|
import com.uber.autodispose.autoDisposable
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
import kotlinx.android.synthetic.main.fragment_timeline.*
|
import kotlinx.android.synthetic.main.fragment_timeline.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -187,6 +193,10 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
||||||
jumpToTop()
|
jumpToTop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onVoteInPoll(position: Int, choices: MutableList<Int>) {
|
||||||
|
viewModel.voteInPoll(position, choices)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun newInstance() = ConversationsFragment()
|
fun newInstance() = ConversationsFragment()
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,6 +67,25 @@ class ConversationsViewModel @Inject constructor(
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun voteInPoll(position: Int, choices: MutableList<Int>) {
|
||||||
|
conversations.value?.getOrNull(position)?.let { conversation ->
|
||||||
|
timelineCases.voteInPoll(conversation.lastStatus.toStatus(), choices)
|
||||||
|
.flatMap { poll ->
|
||||||
|
val newConversation = conversation.copy(
|
||||||
|
lastStatus = conversation.lastStatus.copy(poll = poll)
|
||||||
|
)
|
||||||
|
Single.fromCallable {
|
||||||
|
database.conversationDao().insert(newConversation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.doOnError { t -> Log.w("ConversationViewModel", "Failed to favourite conversation", t) }
|
||||||
|
.subscribe()
|
||||||
|
.addTo(disposables)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
fun expandHiddenStatus(expanded: Boolean, position: Int) {
|
fun expandHiddenStatus(expanded: Boolean, position: Int) {
|
||||||
conversations.value?.getOrNull(position)?.let { conversation ->
|
conversations.value?.getOrNull(position)?.let { conversation ->
|
||||||
val newConversation = conversation.copy(
|
val newConversation = conversation.copy(
|
||||||
|
|
|
@ -30,7 +30,7 @@ import androidx.annotation.NonNull;
|
||||||
|
|
||||||
@Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
@Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
||||||
TimelineAccountEntity.class, ConversationEntity.class
|
TimelineAccountEntity.class, ConversationEntity.class
|
||||||
}, version = 14)
|
}, version = 15)
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
|
|
||||||
public abstract TootDao tootDao();
|
public abstract TootDao tootDao();
|
||||||
|
@ -256,13 +256,6 @@ public abstract class AppDatabase extends RoomDatabase {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public static final Migration MIGRATION_13_14 = new Migration(13, 14) {
|
|
||||||
@Override
|
|
||||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
|
||||||
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsFilter` TEXT NOT NULL DEFAULT '[]'");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public static final Migration MIGRATION_10_13 = new Migration(10, 13) {
|
public static final Migration MIGRATION_10_13 = new Migration(10, 13) {
|
||||||
@Override
|
@Override
|
||||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||||
|
@ -271,4 +264,19 @@ public abstract class AppDatabase extends RoomDatabase {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_13_14 = new Migration(13, 14) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||||
|
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsFilter` TEXT NOT NULL DEFAULT '[]'");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_14_15 = new Migration(14, 15) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||||
|
database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `poll` TEXT");
|
||||||
|
database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_poll` TEXT");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
|
@ -24,6 +24,7 @@ import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity
|
||||||
import com.keylesspalace.tusky.createTabDataFromId
|
import com.keylesspalace.tusky.createTabDataFromId
|
||||||
import com.keylesspalace.tusky.entity.Attachment
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
import com.keylesspalace.tusky.entity.Emoji
|
import com.keylesspalace.tusky.entity.Emoji
|
||||||
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.json.SpannedTypeAdapter
|
import com.keylesspalace.tusky.json.SpannedTypeAdapter
|
||||||
import com.keylesspalace.tusky.util.HtmlUtils
|
import com.keylesspalace.tusky.util.HtmlUtils
|
||||||
|
@ -135,4 +136,14 @@ class Converters {
|
||||||
return HtmlUtils.fromHtml(spannedString)
|
return HtmlUtils.fromHtml(spannedString)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun pollToJson(poll: Poll?): String? {
|
||||||
|
return gson.toJson(poll)
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun jsonToPoll(pollJson: String?): Poll? {
|
||||||
|
return gson.fromJson(pollJson, Poll::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -49,7 +49,8 @@ data class TimelineStatusEntity(
|
||||||
val mentions: String?,
|
val mentions: String?,
|
||||||
val application: String?,
|
val application: String?,
|
||||||
val reblogServerId: String?, // if it has a reblogged status, it's id is stored here
|
val reblogServerId: String?, // if it has a reblogged status, it's id is stored here
|
||||||
val reblogAccountId: String?
|
val reblogAccountId: String?,
|
||||||
|
val poll: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
|
|
33
app/src/main/java/com/keylesspalace/tusky/entity/Poll.kt
Normal file
33
app/src/main/java/com/keylesspalace/tusky/entity/Poll.kt
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
data class Poll(
|
||||||
|
val id: String,
|
||||||
|
@SerializedName("expires_at") val expiresAt: Date?,
|
||||||
|
val expired: Boolean,
|
||||||
|
val multiple: Boolean,
|
||||||
|
@SerializedName("votes_count") val votesCount: Int,
|
||||||
|
val options: List<PollOption>,
|
||||||
|
val voted: Boolean
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun votedCopy(choices: List<Int>): Poll {
|
||||||
|
val newOptions = options.mapIndexed { index, option ->
|
||||||
|
if(choices.contains(index)) {
|
||||||
|
option.copy(votesCount = option.votesCount + 1)
|
||||||
|
} else {
|
||||||
|
option
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return copy(options = newOptions, votesCount = votesCount + 1, voted = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data class PollOption(
|
||||||
|
val title: String,
|
||||||
|
@SerializedName("votes_count") val votesCount: Int
|
||||||
|
)
|
|
@ -39,7 +39,8 @@ data class Status(
|
||||||
@SerializedName("media_attachments") var attachments: ArrayList<Attachment>,
|
@SerializedName("media_attachments") var attachments: ArrayList<Attachment>,
|
||||||
val mentions: Array<Mention>,
|
val mentions: Array<Mention>,
|
||||||
val application: Application?,
|
val application: Application?,
|
||||||
var pinned: Boolean?
|
var pinned: Boolean?,
|
||||||
|
val poll: Poll?
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val actionableId: String
|
val actionableId: String
|
||||||
|
@ -161,5 +162,6 @@ data class Status(
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val MAX_MEDIA_ATTACHMENTS = 4
|
const val MAX_MEDIA_ATTACHMENTS = 4
|
||||||
|
const val MAX_POLL_OPTIONS = 4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@ import com.keylesspalace.tusky.db.AccountEntity;
|
||||||
import com.keylesspalace.tusky.db.AccountManager;
|
import com.keylesspalace.tusky.db.AccountManager;
|
||||||
import com.keylesspalace.tusky.di.Injectable;
|
import com.keylesspalace.tusky.di.Injectable;
|
||||||
import com.keylesspalace.tusky.entity.Notification;
|
import com.keylesspalace.tusky.entity.Notification;
|
||||||
|
import com.keylesspalace.tusky.entity.Poll;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
||||||
import com.keylesspalace.tusky.interfaces.ReselectableFragment;
|
import com.keylesspalace.tusky.interfaces.ReselectableFragment;
|
||||||
|
@ -428,6 +429,24 @@ public class NotificationsFragment extends SFragment implements
|
||||||
updateAdapter();
|
updateAdapter();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onVoteInPoll(int position, @NonNull List<Integer> choices) {
|
||||||
|
final Notification notification = notifications.get(position).asRight();
|
||||||
|
final Status status = notification.getStatus();
|
||||||
|
|
||||||
|
timelineCases.voteInPoll(status, choices)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.as(autoDisposable(from(this)))
|
||||||
|
.subscribe(
|
||||||
|
(newPoll) -> setVoteForPoll(position, newPoll),
|
||||||
|
(t) -> Log.d(TAG,
|
||||||
|
"Failed to vote in poll: " + status.getId(), t)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setVoteForPoll(int position, Poll poll) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMore(@NonNull View view, int position) {
|
public void onMore(@NonNull View view, int position) {
|
||||||
Notification notification = notifications.get(position).asRight();
|
Notification notification = notifications.get(position).asRight();
|
||||||
|
|
|
@ -232,10 +232,6 @@ class SearchFragment : SFragment(), StatusActionListener {
|
||||||
searchRecyclerView.post { searchAdapter.notifyItemChanged(position, updatedStatus) }
|
searchRecyclerView.post { searchAdapter.notifyItemChanged(position, updatedStatus) }
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val TAG = "SearchFragment"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewAccount(id: String) {
|
override fun onViewAccount(id: String) {
|
||||||
val intent = AccountActivity.getIntent(requireContext(), id)
|
val intent = AccountActivity.getIntent(requireContext(), id)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
|
@ -247,4 +243,28 @@ class SearchFragment : SFragment(), StatusActionListener {
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onVoteInPoll(position: Int, choices: MutableList<Int>) {
|
||||||
|
val status = searchAdapter.getStatusAtPosition(position)
|
||||||
|
if (status != null) {
|
||||||
|
timelineCases.voteInPoll(status, choices)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))
|
||||||
|
.subscribe({poll ->
|
||||||
|
val viewData = ViewDataUtils.statusToViewData(
|
||||||
|
status,
|
||||||
|
alwaysShowSensitiveMedia
|
||||||
|
)
|
||||||
|
val newViewData = StatusViewData.Builder(viewData)
|
||||||
|
.setPoll(poll)
|
||||||
|
.createStatusViewData()
|
||||||
|
searchAdapter.updateStatusAtPosition(newViewData, position)
|
||||||
|
|
||||||
|
}, { t -> Log.d(TAG, "Failed to vote in poll " + status.id, t) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "SearchFragment"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,7 @@ import com.keylesspalace.tusky.appstore.UnfollowEvent;
|
||||||
import com.keylesspalace.tusky.db.AccountManager;
|
import com.keylesspalace.tusky.db.AccountManager;
|
||||||
import com.keylesspalace.tusky.di.Injectable;
|
import com.keylesspalace.tusky.di.Injectable;
|
||||||
import com.keylesspalace.tusky.entity.Filter;
|
import com.keylesspalace.tusky.entity.Filter;
|
||||||
|
import com.keylesspalace.tusky.entity.Poll;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
||||||
import com.keylesspalace.tusky.interfaces.ReselectableFragment;
|
import com.keylesspalace.tusky.interfaces.ReselectableFragment;
|
||||||
|
@ -620,6 +621,34 @@ public class TimelineFragment extends SFragment implements
|
||||||
updateAdapter();
|
updateAdapter();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onVoteInPoll(int position, @NonNull List<Integer> choices) {
|
||||||
|
final Status status = statuses.get(position).asRight();
|
||||||
|
|
||||||
|
setVoteForPoll(position, status, status.getPoll().votedCopy(choices));
|
||||||
|
|
||||||
|
timelineCases.voteInPoll(status, choices)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.as(autoDisposable(from(this)))
|
||||||
|
.subscribe(
|
||||||
|
(newPoll) -> setVoteForPoll(position, status, newPoll),
|
||||||
|
(t) -> Log.d(TAG,
|
||||||
|
"Failed to vote in poll: " + status.getId(), t)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setVoteForPoll(int position, Status status, Poll newPoll) {
|
||||||
|
Pair<StatusViewData.Concrete, Integer> actual =
|
||||||
|
findStatusAndPosition(position, status);
|
||||||
|
if (actual == null) return;
|
||||||
|
|
||||||
|
StatusViewData newViewData = new StatusViewData
|
||||||
|
.Builder(actual.first)
|
||||||
|
.setPoll(newPoll)
|
||||||
|
.createStatusViewData();
|
||||||
|
statuses.setPairedItem(actual.second, newViewData);
|
||||||
|
updateAdapter();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMore(@NonNull View view, final int position) {
|
public void onMore(@NonNull View view, final int position) {
|
||||||
super.more(statuses.get(position).asRight(), view, position);
|
super.more(statuses.get(position).asRight(), view, position);
|
||||||
|
|
|
@ -42,6 +42,7 @@ import com.keylesspalace.tusky.appstore.StatusComposedEvent;
|
||||||
import com.keylesspalace.tusky.appstore.StatusDeletedEvent;
|
import com.keylesspalace.tusky.appstore.StatusDeletedEvent;
|
||||||
import com.keylesspalace.tusky.di.Injectable;
|
import com.keylesspalace.tusky.di.Injectable;
|
||||||
import com.keylesspalace.tusky.entity.Card;
|
import com.keylesspalace.tusky.entity.Card;
|
||||||
|
import com.keylesspalace.tusky.entity.Poll;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.entity.StatusContext;
|
import com.keylesspalace.tusky.entity.StatusContext;
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
|
@ -393,6 +394,33 @@ public final class ViewThreadFragment extends SFragment implements
|
||||||
adapter.setStatuses(statuses.getPairedCopy());
|
adapter.setStatuses(statuses.getPairedCopy());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onVoteInPoll(int position, @NonNull List<Integer> choices) {
|
||||||
|
final Status status = statuses.get(position).getActionableStatus();
|
||||||
|
|
||||||
|
setVoteForPoll(position, status.getPoll().votedCopy(choices));
|
||||||
|
|
||||||
|
timelineCases.voteInPoll(status, choices)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.as(autoDisposable(from(this)))
|
||||||
|
.subscribe(
|
||||||
|
(newPoll) -> setVoteForPoll(position, newPoll),
|
||||||
|
(t) -> Log.d(TAG,
|
||||||
|
"Failed to vote in poll: " + status.getId(), t)
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setVoteForPoll(int position, Poll newPoll) {
|
||||||
|
|
||||||
|
StatusViewData.Concrete viewData = statuses.getPairedItem(position);
|
||||||
|
|
||||||
|
StatusViewData.Concrete newViewData = new StatusViewData.Builder(viewData)
|
||||||
|
.setPoll(newPoll)
|
||||||
|
.createStatusViewData();
|
||||||
|
statuses.setPairedItem(position, newViewData);
|
||||||
|
adapter.setItem(position, newViewData, true);
|
||||||
|
}
|
||||||
|
|
||||||
private void removeAllByAccountId(String accountId) {
|
private void removeAllByAccountId(String accountId) {
|
||||||
Status status = null;
|
Status status = null;
|
||||||
if (!statuses.isEmpty()) {
|
if (!statuses.isEmpty()) {
|
||||||
|
|
|
@ -17,6 +17,8 @@ package com.keylesspalace.tusky.interfaces;
|
||||||
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
@ -58,4 +60,6 @@ public interface StatusActionListener extends LinkListener {
|
||||||
*/
|
*/
|
||||||
default void onShowFavs(int position) {}
|
default void onShowFavs(int position) {}
|
||||||
|
|
||||||
|
void onVoteInPoll(int position, @NonNull List<Integer> choices);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import com.keylesspalace.tusky.entity.Filter;
|
||||||
import com.keylesspalace.tusky.entity.Instance;
|
import com.keylesspalace.tusky.entity.Instance;
|
||||||
import com.keylesspalace.tusky.entity.MastoList;
|
import com.keylesspalace.tusky.entity.MastoList;
|
||||||
import com.keylesspalace.tusky.entity.Notification;
|
import com.keylesspalace.tusky.entity.Notification;
|
||||||
|
import com.keylesspalace.tusky.entity.Poll;
|
||||||
import com.keylesspalace.tusky.entity.Relationship;
|
import com.keylesspalace.tusky.entity.Relationship;
|
||||||
import com.keylesspalace.tusky.entity.SearchResults;
|
import com.keylesspalace.tusky.entity.SearchResults;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
@ -382,4 +383,11 @@ public interface MastodonApi {
|
||||||
Call<ResponseBody> deleteFilter(
|
Call<ResponseBody> deleteFilter(
|
||||||
@Path("id") String id
|
@Path("id") String id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST("api/v1/polls/{id}/votes")
|
||||||
|
Single<Poll> voteInPoll(
|
||||||
|
@Path("id") String id,
|
||||||
|
@Field("choices[]") List<Integer> choices
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
package com.keylesspalace.tusky.network
|
package com.keylesspalace.tusky.network
|
||||||
|
|
||||||
import com.keylesspalace.tusky.appstore.*
|
import com.keylesspalace.tusky.appstore.*
|
||||||
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Relationship
|
import com.keylesspalace.tusky.entity.Relationship
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import io.reactivex.Single
|
import io.reactivex.Single
|
||||||
|
@ -25,6 +26,7 @@ import okhttp3.ResponseBody
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.Callback
|
import retrofit2.Callback
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
import java.lang.IllegalStateException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by charlag on 3/24/18.
|
* Created by charlag on 3/24/18.
|
||||||
|
@ -37,6 +39,8 @@ interface TimelineCases {
|
||||||
fun block(id: String)
|
fun block(id: String)
|
||||||
fun delete(id: String)
|
fun delete(id: String)
|
||||||
fun pin(status: Status, pin: Boolean)
|
fun pin(status: Status, pin: Boolean)
|
||||||
|
fun voteInPoll(status: Status, choices: List<Int>): Single<Poll>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class TimelineCasesImpl(
|
class TimelineCasesImpl(
|
||||||
|
@ -116,4 +120,16 @@ class TimelineCasesImpl(
|
||||||
.addTo(this.cancelDisposable)
|
.addTo(this.cancelDisposable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun voteInPoll(status: Status, choices: List<Int>): Single<Poll> {
|
||||||
|
val pollId = status.actionableStatus.poll?.id
|
||||||
|
|
||||||
|
if(pollId == null || choices.isEmpty()) {
|
||||||
|
return Single.error(IllegalStateException())
|
||||||
|
}
|
||||||
|
|
||||||
|
return mastodonApi.voteInPoll(pollId, choices).doAfterSuccess {
|
||||||
|
eventHub.dispatch(PollVoteEvent(status.id, it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -4,9 +4,7 @@ import android.text.SpannedString
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import com.keylesspalace.tusky.db.*
|
import com.keylesspalace.tusky.db.*
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.*
|
||||||
import com.keylesspalace.tusky.entity.Attachment
|
|
||||||
import com.keylesspalace.tusky.entity.Emoji
|
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.repository.TimelineRequestMode.DISK
|
import com.keylesspalace.tusky.repository.TimelineRequestMode.DISK
|
||||||
|
@ -202,6 +200,7 @@ class TimelineRepositoryImpl(
|
||||||
val application = gson.fromJson(status.application, Status.Application::class.java)
|
val application = gson.fromJson(status.application, Status.Application::class.java)
|
||||||
val emojis: List<Emoji> = gson.fromJson(status.emojis,
|
val emojis: List<Emoji> = gson.fromJson(status.emojis,
|
||||||
object : TypeToken<List<Emoji>>() {}.type) ?: listOf()
|
object : TypeToken<List<Emoji>>() {}.type) ?: listOf()
|
||||||
|
val poll: Poll? = gson.fromJson(status.poll, Poll::class.java)
|
||||||
|
|
||||||
val reblog = status.reblogServerId?.let { id ->
|
val reblog = status.reblogServerId?.let { id ->
|
||||||
Status(
|
Status(
|
||||||
|
@ -224,8 +223,8 @@ class TimelineRepositoryImpl(
|
||||||
attachments = attachments,
|
attachments = attachments,
|
||||||
mentions = mentions,
|
mentions = mentions,
|
||||||
application = application,
|
application = application,
|
||||||
pinned = false
|
pinned = false,
|
||||||
|
poll = poll
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val status = if (reblog != null) {
|
val status = if (reblog != null) {
|
||||||
|
@ -249,7 +248,8 @@ class TimelineRepositoryImpl(
|
||||||
attachments = ArrayList(),
|
attachments = ArrayList(),
|
||||||
mentions = arrayOf(),
|
mentions = arrayOf(),
|
||||||
application = null,
|
application = null,
|
||||||
pinned = false
|
pinned = false,
|
||||||
|
poll = null
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Status(
|
Status(
|
||||||
|
@ -272,7 +272,8 @@ class TimelineRepositoryImpl(
|
||||||
attachments = attachments,
|
attachments = attachments,
|
||||||
mentions = mentions,
|
mentions = mentions,
|
||||||
application = application,
|
application = application,
|
||||||
pinned = false
|
pinned = false,
|
||||||
|
poll = poll
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return Either.Right(status)
|
return Either.Right(status)
|
||||||
|
@ -339,8 +340,8 @@ fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity {
|
||||||
mentions = null,
|
mentions = null,
|
||||||
application = null,
|
application = null,
|
||||||
reblogServerId = null,
|
reblogServerId = null,
|
||||||
reblogAccountId = null
|
reblogAccountId = null,
|
||||||
|
poll = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -369,7 +370,8 @@ fun Status.toEntity(timelineUserId: Long,
|
||||||
mentions = actionable.mentions.let(gson::toJson),
|
mentions = actionable.mentions.let(gson::toJson),
|
||||||
application = actionable.let(gson::toJson),
|
application = actionable.let(gson::toJson),
|
||||||
reblogServerId = reblog?.id,
|
reblogServerId = reblog?.id,
|
||||||
reblogAccountId = reblog?.let { this.account.id }
|
reblogAccountId = reblog?.let { this.account.id },
|
||||||
|
poll = actionable.poll.let(gson::toJson)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,57 +20,86 @@ import android.content.Context;
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
|
|
||||||
public class DateUtils {
|
public class DateUtils {
|
||||||
|
|
||||||
|
private static final long SECOND_IN_MILLIS = 1000;
|
||||||
|
private static final long MINUTE_IN_MILLIS = SECOND_IN_MILLIS * 60;
|
||||||
|
private static final long HOUR_IN_MILLIS = MINUTE_IN_MILLIS * 60;
|
||||||
|
private static final long DAY_IN_MILLIS = HOUR_IN_MILLIS * 24;
|
||||||
|
private static final long YEAR_IN_MILLIS = DAY_IN_MILLIS * 365;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a rough duplicate of {@link android.text.format.DateUtils#getRelativeTimeSpanString},
|
* This is a rough duplicate of {@link android.text.format.DateUtils#getRelativeTimeSpanString},
|
||||||
* but even with the FORMAT_ABBREV_RELATIVE flag it wasn't abbreviating enough.
|
* but even with the FORMAT_ABBREV_RELATIVE flag it wasn't abbreviating enough.
|
||||||
*/
|
*/
|
||||||
public static String getRelativeTimeSpanString(Context context, long then, long now) {
|
public static String getRelativeTimeSpanString(Context context, long then, long now) {
|
||||||
final long MINUTE = 60;
|
long span = now - then;
|
||||||
final long HOUR = 60 * MINUTE;
|
|
||||||
final long DAY = 24 * HOUR;
|
|
||||||
final long YEAR = 365 * DAY;
|
|
||||||
long span = (now - then) / 1000;
|
|
||||||
boolean future = false;
|
boolean future = false;
|
||||||
if (span < 0) {
|
if (span < 0) {
|
||||||
future = true;
|
future = true;
|
||||||
span = -span;
|
span = -span;
|
||||||
}
|
}
|
||||||
String format;
|
int format;
|
||||||
if (span < MINUTE) {
|
if (span < MINUTE_IN_MILLIS) {
|
||||||
|
span /= SECOND_IN_MILLIS;
|
||||||
if (future) {
|
if (future) {
|
||||||
format = context.getString(R.string.abbreviated_in_seconds);
|
format = R.string.abbreviated_in_seconds;
|
||||||
} else {
|
} else {
|
||||||
format = context.getString(R.string.abbreviated_seconds_ago);
|
format = R.string.abbreviated_seconds_ago;
|
||||||
}
|
}
|
||||||
} else if (span < HOUR) {
|
} else if (span < HOUR_IN_MILLIS) {
|
||||||
span /= MINUTE;
|
span /= MINUTE_IN_MILLIS;
|
||||||
if (future) {
|
if (future) {
|
||||||
format = context.getString(R.string.abbreviated_in_minutes);
|
format = R.string.abbreviated_in_minutes;
|
||||||
} else {
|
} else {
|
||||||
format = context.getString(R.string.abbreviated_minutes_ago);
|
format = R.string.abbreviated_minutes_ago;
|
||||||
}
|
}
|
||||||
} else if (span < DAY) {
|
} else if (span < DAY_IN_MILLIS) {
|
||||||
span /= HOUR;
|
span /= HOUR_IN_MILLIS;
|
||||||
if (future) {
|
if (future) {
|
||||||
format = context.getString(R.string.abbreviated_in_hours);
|
format = R.string.abbreviated_in_hours;
|
||||||
} else {
|
} else {
|
||||||
format = context.getString(R.string.abbreviated_hours_ago);
|
format = R.string.abbreviated_hours_ago;
|
||||||
}
|
}
|
||||||
} else if (span < YEAR) {
|
} else if (span < YEAR_IN_MILLIS) {
|
||||||
span /= DAY;
|
span /= DAY_IN_MILLIS;
|
||||||
if (future) {
|
if (future) {
|
||||||
format = context.getString(R.string.abbreviated_in_days);
|
format = R.string.abbreviated_in_days;
|
||||||
} else {
|
} else {
|
||||||
format = context.getString(R.string.abbreviated_days_ago);
|
format = R.string.abbreviated_days_ago;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
span /= YEAR;
|
span /= YEAR_IN_MILLIS;
|
||||||
if (future) {
|
if (future) {
|
||||||
format = context.getString(R.string.abbreviated_in_years);
|
format = R.string.abbreviated_in_years;
|
||||||
} else {
|
} else {
|
||||||
format = context.getString(R.string.abbreviated_years_ago);
|
format = R.string.abbreviated_years_ago;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return String.format(format, span);
|
return context.getString(format, span);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String formatDuration(Context context, long then, long now) {
|
||||||
|
long span = then - now;
|
||||||
|
if (span < 0) {
|
||||||
|
span = 0;
|
||||||
|
}
|
||||||
|
int format;
|
||||||
|
if (span < MINUTE_IN_MILLIS) {
|
||||||
|
span /= SECOND_IN_MILLIS;
|
||||||
|
format = R.string.timespan_seconds;
|
||||||
|
} else if (span < HOUR_IN_MILLIS) {
|
||||||
|
span /= MINUTE_IN_MILLIS;
|
||||||
|
format = R.string.timespan_minutes;
|
||||||
|
|
||||||
|
} else if (span < DAY_IN_MILLIS) {
|
||||||
|
span /= HOUR_IN_MILLIS;
|
||||||
|
format = R.string.timespan_hours;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
span /= DAY_IN_MILLIS;
|
||||||
|
format = R.string.timespan_days;
|
||||||
|
}
|
||||||
|
return context.getString(format, span);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,8 +63,8 @@ public final class ViewDataUtils {
|
||||||
SmartLengthInputFilter.LENGTH_DEFAULT
|
SmartLengthInputFilter.LENGTH_DEFAULT
|
||||||
))
|
))
|
||||||
.setCollapsed(true)
|
.setCollapsed(true)
|
||||||
|
.setPoll(visibleStatus.getPoll())
|
||||||
.setIsBot(visibleStatus.getAccount().getBot())
|
.setIsBot(visibleStatus.getAccount().getBot())
|
||||||
|
|
||||||
.createStatusViewData();
|
.createStatusViewData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ import android.text.Spanned;
|
||||||
import com.keylesspalace.tusky.entity.Attachment;
|
import com.keylesspalace.tusky.entity.Attachment;
|
||||||
import com.keylesspalace.tusky.entity.Card;
|
import com.keylesspalace.tusky.entity.Card;
|
||||||
import com.keylesspalace.tusky.entity.Emoji;
|
import com.keylesspalace.tusky.entity.Emoji;
|
||||||
|
import com.keylesspalace.tusky.entity.Poll;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -87,6 +88,8 @@ public abstract class StatusViewData {
|
||||||
private final Card card;
|
private final Card card;
|
||||||
private final boolean isCollapsible; /** Whether the status meets the requirement to be collapse */
|
private final boolean isCollapsible; /** Whether the status meets the requirement to be collapse */
|
||||||
final boolean isCollapsed; /** Whether the status is shown partially or fully */
|
final boolean isCollapsed; /** Whether the status is shown partially or fully */
|
||||||
|
@Nullable
|
||||||
|
private final Poll poll;
|
||||||
private final boolean isBot;
|
private final boolean isBot;
|
||||||
|
|
||||||
public Concrete(String id, Spanned content, boolean reblogged, boolean favourited,
|
public Concrete(String id, Spanned content, boolean reblogged, boolean favourited,
|
||||||
|
@ -96,7 +99,8 @@ public abstract class StatusViewData {
|
||||||
Date createdAt, int reblogsCount, int favouritesCount, @Nullable String inReplyToId,
|
Date createdAt, int reblogsCount, int favouritesCount, @Nullable String inReplyToId,
|
||||||
@Nullable Status.Mention[] mentions, String senderId, boolean rebloggingEnabled,
|
@Nullable Status.Mention[] mentions, String senderId, boolean rebloggingEnabled,
|
||||||
Status.Application application, List<Emoji> statusEmojis, List<Emoji> accountEmojis, @Nullable Card card,
|
Status.Application application, List<Emoji> statusEmojis, List<Emoji> accountEmojis, @Nullable Card card,
|
||||||
boolean isCollapsible, boolean isCollapsed, boolean isBot) {
|
boolean isCollapsible, boolean isCollapsed, @Nullable Poll poll, boolean isBot) {
|
||||||
|
|
||||||
this.id = id;
|
this.id = id;
|
||||||
if (Build.VERSION.SDK_INT == 23) {
|
if (Build.VERSION.SDK_INT == 23) {
|
||||||
// https://github.com/tuskyapp/Tusky/issues/563
|
// https://github.com/tuskyapp/Tusky/issues/563
|
||||||
|
@ -132,6 +136,7 @@ public abstract class StatusViewData {
|
||||||
this.card = card;
|
this.card = card;
|
||||||
this.isCollapsible = isCollapsible;
|
this.isCollapsible = isCollapsible;
|
||||||
this.isCollapsed = isCollapsed;
|
this.isCollapsed = isCollapsed;
|
||||||
|
this.poll = poll;
|
||||||
this.isBot = isBot;
|
this.isBot = isBot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -267,6 +272,11 @@ public abstract class StatusViewData {
|
||||||
return isCollapsed;
|
return isCollapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public Poll getPoll() {
|
||||||
|
return poll;
|
||||||
|
}
|
||||||
|
|
||||||
@Override public long getViewDataId() {
|
@Override public long getViewDataId() {
|
||||||
// Chance of collision is super low and impact of mistake is low as well
|
// Chance of collision is super low and impact of mistake is low as well
|
||||||
return id.hashCode();
|
return id.hashCode();
|
||||||
|
@ -302,7 +312,8 @@ public abstract class StatusViewData {
|
||||||
Objects.equals(application, concrete.application) &&
|
Objects.equals(application, concrete.application) &&
|
||||||
Objects.equals(statusEmojis, concrete.statusEmojis) &&
|
Objects.equals(statusEmojis, concrete.statusEmojis) &&
|
||||||
Objects.equals(accountEmojis, concrete.accountEmojis) &&
|
Objects.equals(accountEmojis, concrete.accountEmojis) &&
|
||||||
Objects.equals(card, concrete.card)
|
Objects.equals(card, concrete.card) &&
|
||||||
|
Objects.equals(poll, concrete.poll)
|
||||||
&& isCollapsed == concrete.isCollapsed;
|
&& isCollapsed == concrete.isCollapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -407,6 +418,7 @@ public abstract class StatusViewData {
|
||||||
private Card card;
|
private Card card;
|
||||||
private boolean isCollapsible; /** Whether the status meets the requirement to be collapsed */
|
private boolean isCollapsible; /** Whether the status meets the requirement to be collapsed */
|
||||||
private boolean isCollapsed; /** Whether the status is shown partially or fully */
|
private boolean isCollapsed; /** Whether the status is shown partially or fully */
|
||||||
|
private Poll poll;
|
||||||
private boolean isBot;
|
private boolean isBot;
|
||||||
|
|
||||||
public Builder() {
|
public Builder() {
|
||||||
|
@ -441,6 +453,7 @@ public abstract class StatusViewData {
|
||||||
card = viewData.getCard();
|
card = viewData.getCard();
|
||||||
isCollapsible = viewData.isCollapsible();
|
isCollapsible = viewData.isCollapsible();
|
||||||
isCollapsed = viewData.isCollapsed();
|
isCollapsed = viewData.isCollapsed();
|
||||||
|
poll = viewData.poll;
|
||||||
isBot = viewData.isBot();
|
isBot = viewData.isBot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -603,6 +616,11 @@ public abstract class StatusViewData {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder setPoll(Poll poll) {
|
||||||
|
this.poll = poll;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public StatusViewData.Concrete createStatusViewData() {
|
public StatusViewData.Concrete createStatusViewData() {
|
||||||
if (this.statusEmojis == null) statusEmojis = Collections.emptyList();
|
if (this.statusEmojis == null) statusEmojis = Collections.emptyList();
|
||||||
if (this.accountEmojis == null) accountEmojis = Collections.emptyList();
|
if (this.accountEmojis == null) accountEmojis = Collections.emptyList();
|
||||||
|
@ -612,7 +630,7 @@ public abstract class StatusViewData {
|
||||||
attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded,
|
attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded,
|
||||||
isShowingContent, userFullName, nickname, avatar, createdAt, reblogsCount,
|
isShowingContent, userFullName, nickname, avatar, createdAt, reblogsCount,
|
||||||
favouritesCount, inReplyToId, mentions, senderId, rebloggingEnabled, application,
|
favouritesCount, inReplyToId, mentions, senderId, rebloggingEnabled, application,
|
||||||
statusEmojis, accountEmojis, card, isCollapsible, isCollapsed, isBot);
|
statusEmojis, accountEmojis, card, isCollapsible, isCollapsed, poll, isBot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
6
app/src/main/res/drawable/poll_option_background.xml
Normal file
6
app/src/main/res/drawable/poll_option_background.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<clip
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:drawable="@drawable/poll_option_shape"
|
||||||
|
android:clipOrientation="horizontal"
|
||||||
|
android:gravity="left|clip_horizontal|fill_vertical"/>
|
6
app/src/main/res/drawable/poll_option_shape.xml
Normal file
6
app/src/main/res/drawable/poll_option_shape.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="?attr/pollOptionBackgroundColor" />
|
||||||
|
</shape>
|
|
@ -341,6 +341,149 @@
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/status_poll_option_result_0"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:background="@drawable/poll_option_background"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:lines="1"
|
||||||
|
android:paddingStart="6dp"
|
||||||
|
android:paddingTop="2dp"
|
||||||
|
android:paddingEnd="6dp"
|
||||||
|
android:paddingBottom="2dp"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/status_media_preview_container"
|
||||||
|
tools:text="40%" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/status_poll_option_result_1"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:background="@drawable/poll_option_background"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:lines="1"
|
||||||
|
android:paddingStart="6dp"
|
||||||
|
android:paddingTop="2dp"
|
||||||
|
android:paddingEnd="6dp"
|
||||||
|
android:paddingBottom="2dp"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/status_poll_option_result_0"
|
||||||
|
tools:text="10%" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/status_poll_option_result_2"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:background="@drawable/poll_option_background"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:lines="1"
|
||||||
|
android:paddingStart="6dp"
|
||||||
|
android:paddingTop="2dp"
|
||||||
|
android:paddingEnd="6dp"
|
||||||
|
android:paddingBottom="2dp"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/status_poll_option_result_1"
|
||||||
|
tools:text="20%" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/status_poll_option_result_3"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:background="@drawable/poll_option_background"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:lines="1"
|
||||||
|
android:paddingStart="6dp"
|
||||||
|
android:paddingTop="2dp"
|
||||||
|
android:paddingEnd="6dp"
|
||||||
|
android:paddingBottom="2dp"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/status_poll_option_result_2"
|
||||||
|
tools:text="30%" />
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/status_poll_radio_group"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/status_poll_option_result_3">
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/status_poll_radio_button_0"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
tools:text="Option 1" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/status_poll_radio_button_1"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
tools:text="Option 2" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/status_poll_radio_button_2"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
tools:text="Option 3" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/status_poll_radio_button_3"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
tools:text="Option 4" />
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
<!-- using AppCompatButton because we don't want the inflater to turn it into a MaterialButton -->
|
||||||
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
|
android:id="@+id/status_poll_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/content_warning_button"
|
||||||
|
android:gravity="center"
|
||||||
|
android:minWidth="150dp"
|
||||||
|
android:minHeight="0dp"
|
||||||
|
android:paddingLeft="16dp"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:paddingRight="16dp"
|
||||||
|
android:paddingBottom="4dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="@string/poll_vote"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/status_poll_radio_group" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/status_poll_description"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/status_poll_button"
|
||||||
|
tools:text="7 votes • 7 hours remaining" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/status_reply"
|
android:id="@+id/status_reply"
|
||||||
style="?attr/image_button_style"
|
style="?attr/image_button_style"
|
||||||
|
@ -354,7 +497,7 @@
|
||||||
app:layout_constraintEnd_toStartOf="@id/status_favourite"
|
app:layout_constraintEnd_toStartOf="@id/status_favourite"
|
||||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||||
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||||
app:layout_constraintTop_toBottomOf="@id/status_media_preview_container"
|
app:layout_constraintTop_toBottomOf="@id/status_poll_description"
|
||||||
app:srcCompat="@drawable/ic_reply_24dp" />
|
app:srcCompat="@drawable/ic_reply_24dp" />
|
||||||
|
|
||||||
<at.connyduck.sparkbutton.SparkButton
|
<at.connyduck.sparkbutton.SparkButton
|
||||||
|
|
|
@ -327,6 +327,149 @@
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/status_poll_option_result_0"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:background="@drawable/poll_option_background"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:lines="1"
|
||||||
|
android:paddingStart="6dp"
|
||||||
|
android:paddingTop="2dp"
|
||||||
|
android:paddingEnd="6dp"
|
||||||
|
android:paddingBottom="2dp"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/status_media_preview_container"
|
||||||
|
tools:text="40%" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/status_poll_option_result_1"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:background="@drawable/poll_option_background"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:lines="1"
|
||||||
|
android:paddingStart="6dp"
|
||||||
|
android:paddingTop="2dp"
|
||||||
|
android:paddingEnd="6dp"
|
||||||
|
android:paddingBottom="2dp"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/status_poll_option_result_0"
|
||||||
|
tools:text="10%" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/status_poll_option_result_2"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:background="@drawable/poll_option_background"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:lines="1"
|
||||||
|
android:paddingStart="6dp"
|
||||||
|
android:paddingTop="2dp"
|
||||||
|
android:paddingEnd="6dp"
|
||||||
|
android:paddingBottom="2dp"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/status_poll_option_result_1"
|
||||||
|
tools:text="20%" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/status_poll_option_result_3"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:background="@drawable/poll_option_background"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:lines="1"
|
||||||
|
android:paddingStart="6dp"
|
||||||
|
android:paddingTop="2dp"
|
||||||
|
android:paddingEnd="6dp"
|
||||||
|
android:paddingBottom="2dp"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/status_poll_option_result_2"
|
||||||
|
tools:text="30%" />
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/status_poll_radio_group"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/status_poll_option_result_3">
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/status_poll_radio_button_0"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
tools:text="Option 1" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/status_poll_radio_button_1"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
tools:text="Option 2" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/status_poll_radio_button_2"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
tools:text="Option 3" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/status_poll_radio_button_3"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
tools:text="Option 4" />
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
<!-- using AppCompatButton because we don't want the inflater to turn it into a MaterialButton -->
|
||||||
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
|
android:id="@+id/status_poll_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/content_warning_button"
|
||||||
|
android:gravity="center"
|
||||||
|
android:minWidth="150dp"
|
||||||
|
android:minHeight="0dp"
|
||||||
|
android:paddingLeft="16dp"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:paddingRight="16dp"
|
||||||
|
android:paddingBottom="4dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="@string/poll_vote"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/status_poll_radio_group" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/status_poll_description"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/status_poll_button"
|
||||||
|
tools:text="7 votes • 7 hours remaining" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/status_reply"
|
android:id="@+id/status_reply"
|
||||||
style="?attr/image_button_style"
|
style="?attr/image_button_style"
|
||||||
|
@ -341,7 +484,7 @@
|
||||||
app:layout_constraintEnd_toStartOf="@id/status_inset"
|
app:layout_constraintEnd_toStartOf="@id/status_inset"
|
||||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||||
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||||
app:layout_constraintTop_toBottomOf="@id/status_media_preview_container"
|
app:layout_constraintTop_toBottomOf="@id/status_poll_description"
|
||||||
app:srcCompat="@drawable/ic_reply_24dp" />
|
app:srcCompat="@drawable/ic_reply_24dp" />
|
||||||
|
|
||||||
<at.connyduck.sparkbutton.SparkButton
|
<at.connyduck.sparkbutton.SparkButton
|
||||||
|
|
|
@ -335,6 +335,151 @@
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/status_poll_option_result_0"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:background="@drawable/poll_option_background"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:lines="1"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
android:paddingBottom="4dp"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/status_media_preview_container"
|
||||||
|
tools:text="40%" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/status_poll_option_result_1"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:background="@drawable/poll_option_background"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:lines="1"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
android:paddingBottom="4dp"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/status_poll_option_result_0"
|
||||||
|
tools:text="10%" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/status_poll_option_result_2"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:background="@drawable/poll_option_background"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:lines="1"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
android:paddingBottom="4dp"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/status_poll_option_result_1"
|
||||||
|
tools:text="20%" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/status_poll_option_result_3"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:background="@drawable/poll_option_background"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:lines="1"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
android:paddingBottom="4dp"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/status_poll_option_result_2"
|
||||||
|
tools:text="30%" />
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/status_poll_radio_group"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/status_poll_option_result_3">
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/status_poll_radio_button_0"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
tools:text="Option 1" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/status_poll_radio_button_1"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
tools:text="Option 2" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/status_poll_radio_button_2"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
tools:text="Option 3" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/status_poll_radio_button_3"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
tools:text="Option 4" />
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
<!-- using AppCompatButton because we don't want the inflater to turn it into a MaterialButton -->
|
||||||
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
|
android:id="@+id/status_poll_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:background="?attr/content_warning_button"
|
||||||
|
android:gravity="center"
|
||||||
|
android:minWidth="150dp"
|
||||||
|
android:minHeight="0dp"
|
||||||
|
android:paddingLeft="16dp"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:paddingRight="16dp"
|
||||||
|
android:paddingBottom="4dp"
|
||||||
|
android:text="@string/poll_vote"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/status_poll_radio_group" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/status_poll_description"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/status_poll_button"
|
||||||
|
tools:text="7 votes • 7 hours remaining" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/status_timestamp_info"
|
android:id="@+id/status_timestamp_info"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
@ -346,7 +491,7 @@
|
||||||
android:textSize="?attr/status_text_medium"
|
android:textSize="?attr/status_text_medium"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/status_media_preview_container"
|
app:layout_constraintTop_toBottomOf="@id/status_poll_description"
|
||||||
tools:text="21 Dec 2018 18:45" />
|
tools:text="21 Dec 2018 18:45" />
|
||||||
|
|
||||||
<View
|
<View
|
||||||
|
@ -355,8 +500,8 @@
|
||||||
android:layout_height="1dp"
|
android:layout_height="1dp"
|
||||||
android:layout_below="@id/status_timestamp_info"
|
android:layout_below="@id/status_timestamp_info"
|
||||||
android:layout_marginTop="6dp"
|
android:layout_marginTop="6dp"
|
||||||
android:importantForAccessibility="no"
|
|
||||||
android:background="?android:attr/listDivider"
|
android:background="?android:attr/listDivider"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
android:paddingStart="16dp"
|
android:paddingStart="16dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingEnd="16dp"
|
||||||
app:layout_constraintTop_toBottomOf="@id/status_timestamp_info" />
|
app:layout_constraintTop_toBottomOf="@id/status_timestamp_info" />
|
||||||
|
@ -403,8 +548,8 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="1dp"
|
android:layout_height="1dp"
|
||||||
android:layout_marginTop="6dp"
|
android:layout_marginTop="6dp"
|
||||||
android:importantForAccessibility="no"
|
|
||||||
android:background="?android:attr/listDivider"
|
android:background="?android:attr/listDivider"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
android:paddingStart="16dp"
|
android:paddingStart="16dp"
|
||||||
android:paddingEnd="16dp"
|
android:paddingEnd="16dp"
|
||||||
app:layout_constraintTop_toBottomOf="@id/status_counters_barrier" />
|
app:layout_constraintTop_toBottomOf="@id/status_counters_barrier" />
|
||||||
|
|
|
@ -78,6 +78,8 @@
|
||||||
|
|
||||||
<item name="minTouchTargetSize">32dp</item> <!-- this affects RadioButton size -->
|
<item name="minTouchTargetSize">32dp</item> <!-- this affects RadioButton size -->
|
||||||
|
|
||||||
|
<item name="pollOptionBackgroundColor">@color/color_primary_dark</item>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="TuskyImageButton.Dark" parent="@style/Widget.MaterialComponents.Button.UnelevatedButton">
|
<style name="TuskyImageButton.Dark" parent="@style/Widget.MaterialComponents.Button.UnelevatedButton">
|
||||||
|
|
|
@ -45,4 +45,6 @@
|
||||||
<attr name="status_text_medium" format="dimension" />
|
<attr name="status_text_medium" format="dimension" />
|
||||||
<attr name="status_text_large" format="dimension" />
|
<attr name="status_text_large" format="dimension" />
|
||||||
|
|
||||||
|
<attr name="pollOptionBackgroundColor" format="reference|color" />
|
||||||
|
|
||||||
</resources>
|
</resources>
|
|
@ -317,6 +317,12 @@
|
||||||
<string name="abbreviated_minutes_ago">%dm</string>
|
<string name="abbreviated_minutes_ago">%dm</string>
|
||||||
<string name="abbreviated_seconds_ago">%ds</string>
|
<string name="abbreviated_seconds_ago">%ds</string>
|
||||||
|
|
||||||
|
<!--These are for timestamps on polls -->
|
||||||
|
<string name="timespan_days">%d days</string>
|
||||||
|
<string name="timespan_hours">%d hours</string>
|
||||||
|
<string name="timespan_minutes">%d minutes</string>
|
||||||
|
<string name="timespan_seconds">%d seconds</string>
|
||||||
|
|
||||||
<string name="follows_you">Follows you</string>
|
<string name="follows_you">Follows you</string>
|
||||||
<string name="pref_title_alway_show_sensitive_media">Always show sensitive content</string>
|
<string name="pref_title_alway_show_sensitive_media">Always show sensitive content</string>
|
||||||
<string name="title_media">Media</string>
|
<string name="title_media">Media</string>
|
||||||
|
@ -476,4 +482,21 @@
|
||||||
|
|
||||||
<string name="notification_clear_text">Are you sure you want to permanently clear all your notifications?</string>
|
<string name="notification_clear_text">Are you sure you want to permanently clear all your notifications?</string>
|
||||||
|
|
||||||
|
<string name="poll_info_format">
|
||||||
|
<!-- 15 votes • 1 hour left -->
|
||||||
|
%1$s • %2$s</string>
|
||||||
|
<plurals name="poll_info_votes">
|
||||||
|
<item quantity="one">%s vote</item>
|
||||||
|
<item quantity="other">%s votes</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="poll_info_time_relative">%s left</string>
|
||||||
|
<string name="poll_info_time_absolute">ends at %s</string>
|
||||||
|
<string name="poll_info_closed">closed</string>
|
||||||
|
<string name="poll_option_format">
|
||||||
|
<!-- 15% vote for this! -->
|
||||||
|
<b>%1$d%%</b> %2$s</string>
|
||||||
|
|
||||||
|
<string name="poll_vote">Vote</string>
|
||||||
|
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -140,6 +140,8 @@
|
||||||
|
|
||||||
<item name="minTouchTargetSize">32dp</item> <!-- this affects RadioButton size -->
|
<item name="minTouchTargetSize">32dp</item> <!-- this affects RadioButton size -->
|
||||||
|
|
||||||
|
<item name="pollOptionBackgroundColor">@color/color_primary_dark_light</item>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="TuskyImageButton.Light" parent="@style/Widget.MaterialComponents.Button.UnelevatedButton">
|
<style name="TuskyImageButton.Light" parent="@style/Widget.MaterialComponents.Button.UnelevatedButton">
|
||||||
|
|
|
@ -84,7 +84,8 @@ class BottomSheetActivityTest {
|
||||||
ArrayList(),
|
ArrayList(),
|
||||||
arrayOf(),
|
arrayOf(),
|
||||||
null,
|
null,
|
||||||
pinned = false
|
pinned = false,
|
||||||
|
poll = null
|
||||||
)
|
)
|
||||||
private val statusCallback = FakeSearchResults(status)
|
private val statusCallback = FakeSearchResults(status)
|
||||||
|
|
||||||
|
|
|
@ -305,7 +305,8 @@ class TimelineRepositoryTest {
|
||||||
inReplyToId = null,
|
inReplyToId = null,
|
||||||
pinned = false,
|
pinned = false,
|
||||||
reblog = null,
|
reblog = null,
|
||||||
url = "http://example.com/statuses/$id"
|
url = "http://example.com/statuses/$id",
|
||||||
|
poll = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue