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
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_8_9, AppDatabase.MIGRATION_9_10, AppDatabase.MIGRATION_10_11,
|
||||
AppDatabase.MIGRATION_11_12, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13,
|
||||
AppDatabase.MIGRATION_13_14)
|
||||
AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15)
|
||||
.build();
|
||||
accountManager = new AccountManager(appDatabase);
|
||||
serviceLocator = new ServiceLocator() {
|
||||
|
@ -143,6 +143,7 @@ public class SearchResultsAdapter extends RecyclerView.Adapter {
|
||||
|
||||
public void updateStatusAtPosition(StatusViewData.Concrete status, int position) {
|
||||
concreteStatusList.set(position - accountList.size(), status);
|
||||
notifyItemChanged(position);
|
||||
}
|
||||
|
||||
public void removeStatusAtPosition(int position) {
|
||||
|
@ -7,8 +7,11 @@ import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.TextView;
|
||||
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.MetaData;
|
||||
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.interfaces.StatusActionListener;
|
||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||
@ -31,6 +36,7 @@ import com.mikepenz.iconics.utils.Utils;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@ -70,6 +76,12 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||
public TextView content;
|
||||
public TextView contentWarningDescription;
|
||||
|
||||
private TextView[] pollResults;
|
||||
private TextView pollDescription;
|
||||
private RadioGroup pollRadioGroup;
|
||||
private RadioButton[] pollRadioOptions;
|
||||
private Button pollButton;
|
||||
|
||||
private boolean useAbsoluteTime;
|
||||
private SimpleDateFormat shortSdf;
|
||||
private SimpleDateFormat longSdf;
|
||||
@ -109,6 +121,25 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||
contentWarningButton = itemView.findViewById(R.id.status_content_warning_button);
|
||||
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;
|
||||
shortSdf = new SimpleDateFormat("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) {
|
||||
String time;
|
||||
if (createdAt != null) {
|
||||
if (System.currentTimeMillis() - createdAt.getTime() > 86400000L) {
|
||||
time = longSdf.format(createdAt);
|
||||
} else {
|
||||
if (android.text.format.DateUtils.isToday(createdAt.getTime())) {
|
||||
time = shortSdf.format(createdAt);
|
||||
} else {
|
||||
time = longSdf.format(createdAt);
|
||||
}
|
||||
} else {
|
||||
time = "??:??:??";
|
||||
@ -588,6 +619,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||
setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getStatusEmojis(), listener);
|
||||
|
||||
setContentDescription(status);
|
||||
|
||||
setupPoll(status.getPoll(),status.getStatusEmojis(), listener);
|
||||
|
||||
// Workaround for RecyclerView 1.0.0 / androidx.core 1.0.0
|
||||
// RecyclerView tries to set AccessibilityDelegateCompat to null
|
||||
// but ViewCompat code replaces is with the default one. RecyclerView never
|
||||
@ -717,4 +751,124 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||
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.entity.Account
|
||||
import com.keylesspalace.tusky.entity.Poll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
|
||||
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 ProfileEditedEvent(val newProfileData: Account) : 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 expanded: 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 */
|
||||
@ -104,6 +105,7 @@ data class ConversationStatusEntity(
|
||||
if (expanded != other.expanded) return false
|
||||
if (collapsible != other.collapsible) return false
|
||||
if (collapsed != other.collapsed) return false
|
||||
if (poll != other.poll) return false
|
||||
|
||||
return true
|
||||
}
|
||||
@ -127,6 +129,7 @@ data class ConversationStatusEntity(
|
||||
result = 31 * result + expanded.hashCode()
|
||||
result = 31 * result + collapsible.hashCode()
|
||||
result = 31 * result + collapsed.hashCode()
|
||||
result = 31 * result + poll.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
@ -151,7 +154,8 @@ data class ConversationStatusEntity(
|
||||
attachments = attachments,
|
||||
mentions = mentions,
|
||||
application = null,
|
||||
pinned = false)
|
||||
pinned = false,
|
||||
poll = poll)
|
||||
}
|
||||
}
|
||||
|
||||
@ -172,7 +176,8 @@ fun Status.toEntity() =
|
||||
false,
|
||||
false,
|
||||
!SmartLengthInputFilter.hasBadRatio(content, SmartLengthInputFilter.LENGTH_DEFAULT),
|
||||
true
|
||||
true,
|
||||
poll
|
||||
)
|
||||
|
||||
|
||||
|
@ -102,6 +102,8 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
||||
|
||||
setAvatars(conversation.getAccounts());
|
||||
|
||||
setupPoll(status.getPoll(), status.getEmojis(), listener);
|
||||
|
||||
}
|
||||
|
||||
private void setConversationName(List<ConversationAccountEntity> accounts) {
|
||||
|
@ -18,9 +18,11 @@ package com.keylesspalace.tusky.components.conversation
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
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.ViewModelFactory
|
||||
import com.keylesspalace.tusky.fragment.SFragment
|
||||
import com.keylesspalace.tusky.fragment.SearchFragment
|
||||
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
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 javax.inject.Inject
|
||||
|
||||
@ -187,6 +193,10 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
||||
jumpToTop()
|
||||
}
|
||||
|
||||
override fun onVoteInPoll(position: Int, choices: MutableList<Int>) {
|
||||
viewModel.voteInPoll(position, choices)
|
||||
}
|
||||
|
||||
companion object {
|
||||
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) {
|
||||
conversations.value?.getOrNull(position)?.let { conversation ->
|
||||
val newConversation = conversation.copy(
|
||||
|
@ -30,7 +30,7 @@ import androidx.annotation.NonNull;
|
||||
|
||||
@Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
||||
TimelineAccountEntity.class, ConversationEntity.class
|
||||
}, version = 14)
|
||||
}, version = 15)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
|
||||
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) {
|
||||
@Override
|
||||
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.entity.Attachment
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.Poll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.json.SpannedTypeAdapter
|
||||
import com.keylesspalace.tusky.util.HtmlUtils
|
||||
@ -135,4 +136,14 @@ class Converters {
|
||||
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 application: String?,
|
||||
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(
|
||||
|
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>,
|
||||
val mentions: Array<Mention>,
|
||||
val application: Application?,
|
||||
var pinned: Boolean?
|
||||
var pinned: Boolean?,
|
||||
val poll: Poll?
|
||||
) {
|
||||
|
||||
val actionableId: String
|
||||
@ -161,5 +162,6 @@ data class Status(
|
||||
|
||||
companion object {
|
||||
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.di.Injectable;
|
||||
import com.keylesspalace.tusky.entity.Notification;
|
||||
import com.keylesspalace.tusky.entity.Poll;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
||||
import com.keylesspalace.tusky.interfaces.ReselectableFragment;
|
||||
@ -428,6 +429,24 @@ public class NotificationsFragment extends SFragment implements
|
||||
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
|
||||
public void onMore(@NonNull View view, int position) {
|
||||
Notification notification = notifications.get(position).asRight();
|
||||
|
@ -232,10 +232,6 @@ class SearchFragment : SFragment(), StatusActionListener {
|
||||
searchRecyclerView.post { searchAdapter.notifyItemChanged(position, updatedStatus) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "SearchFragment"
|
||||
}
|
||||
|
||||
override fun onViewAccount(id: String) {
|
||||
val intent = AccountActivity.getIntent(requireContext(), id)
|
||||
startActivity(intent)
|
||||
@ -247,4 +243,28 @@ class SearchFragment : SFragment(), StatusActionListener {
|
||||
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.di.Injectable;
|
||||
import com.keylesspalace.tusky.entity.Filter;
|
||||
import com.keylesspalace.tusky.entity.Poll;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
||||
import com.keylesspalace.tusky.interfaces.ReselectableFragment;
|
||||
@ -620,6 +621,34 @@ public class TimelineFragment extends SFragment implements
|
||||
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
|
||||
public void onMore(@NonNull View view, final int 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.di.Injectable;
|
||||
import com.keylesspalace.tusky.entity.Card;
|
||||
import com.keylesspalace.tusky.entity.Poll;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.entity.StatusContext;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
@ -393,6 +394,33 @@ public final class ViewThreadFragment extends SFragment implements
|
||||
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) {
|
||||
Status status = null;
|
||||
if (!statuses.isEmpty()) {
|
||||
|
@ -17,6 +17,8 @@ package com.keylesspalace.tusky.interfaces;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@ -58,4 +60,6 @@ public interface StatusActionListener extends LinkListener {
|
||||
*/
|
||||
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.MastoList;
|
||||
import com.keylesspalace.tusky.entity.Notification;
|
||||
import com.keylesspalace.tusky.entity.Poll;
|
||||
import com.keylesspalace.tusky.entity.Relationship;
|
||||
import com.keylesspalace.tusky.entity.SearchResults;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
@ -382,4 +383,11 @@ public interface MastodonApi {
|
||||
Call<ResponseBody> deleteFilter(
|
||||
@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
|
||||
|
||||
import com.keylesspalace.tusky.appstore.*
|
||||
import com.keylesspalace.tusky.entity.Poll
|
||||
import com.keylesspalace.tusky.entity.Relationship
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import io.reactivex.Single
|
||||
@ -25,6 +26,7 @@ import okhttp3.ResponseBody
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.lang.IllegalStateException
|
||||
|
||||
/**
|
||||
* Created by charlag on 3/24/18.
|
||||
@ -37,6 +39,8 @@ interface TimelineCases {
|
||||
fun block(id: String)
|
||||
fun delete(id: String)
|
||||
fun pin(status: Status, pin: Boolean)
|
||||
fun voteInPoll(status: Status, choices: List<Int>): Single<Poll>
|
||||
|
||||
}
|
||||
|
||||
class TimelineCasesImpl(
|
||||
@ -116,4 +120,16 @@ class TimelineCasesImpl(
|
||||
.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.reflect.TypeToken
|
||||
import com.keylesspalace.tusky.db.*
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.*
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.repository.TimelineRequestMode.DISK
|
||||
@ -202,6 +200,7 @@ class TimelineRepositoryImpl(
|
||||
val application = gson.fromJson(status.application, Status.Application::class.java)
|
||||
val emojis: List<Emoji> = gson.fromJson(status.emojis,
|
||||
object : TypeToken<List<Emoji>>() {}.type) ?: listOf()
|
||||
val poll: Poll? = gson.fromJson(status.poll, Poll::class.java)
|
||||
|
||||
val reblog = status.reblogServerId?.let { id ->
|
||||
Status(
|
||||
@ -224,8 +223,8 @@ class TimelineRepositoryImpl(
|
||||
attachments = attachments,
|
||||
mentions = mentions,
|
||||
application = application,
|
||||
pinned = false
|
||||
|
||||
pinned = false,
|
||||
poll = poll
|
||||
)
|
||||
}
|
||||
val status = if (reblog != null) {
|
||||
@ -249,7 +248,8 @@ class TimelineRepositoryImpl(
|
||||
attachments = ArrayList(),
|
||||
mentions = arrayOf(),
|
||||
application = null,
|
||||
pinned = false
|
||||
pinned = false,
|
||||
poll = null
|
||||
)
|
||||
} else {
|
||||
Status(
|
||||
@ -272,7 +272,8 @@ class TimelineRepositoryImpl(
|
||||
attachments = attachments,
|
||||
mentions = mentions,
|
||||
application = application,
|
||||
pinned = false
|
||||
pinned = false,
|
||||
poll = poll
|
||||
)
|
||||
}
|
||||
return Either.Right(status)
|
||||
@ -339,8 +340,8 @@ fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity {
|
||||
mentions = null,
|
||||
application = null,
|
||||
reblogServerId = null,
|
||||
reblogAccountId = null
|
||||
|
||||
reblogAccountId = null,
|
||||
poll = null
|
||||
)
|
||||
}
|
||||
|
||||
@ -369,7 +370,8 @@ fun Status.toEntity(timelineUserId: Long,
|
||||
mentions = actionable.mentions.let(gson::toJson),
|
||||
application = actionable.let(gson::toJson),
|
||||
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;
|
||||
|
||||
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},
|
||||
* but even with the FORMAT_ABBREV_RELATIVE flag it wasn't abbreviating enough.
|
||||
*/
|
||||
public static String getRelativeTimeSpanString(Context context, long then, long now) {
|
||||
final long MINUTE = 60;
|
||||
final long HOUR = 60 * MINUTE;
|
||||
final long DAY = 24 * HOUR;
|
||||
final long YEAR = 365 * DAY;
|
||||
long span = (now - then) / 1000;
|
||||
long span = now - then;
|
||||
boolean future = false;
|
||||
if (span < 0) {
|
||||
future = true;
|
||||
span = -span;
|
||||
}
|
||||
String format;
|
||||
if (span < MINUTE) {
|
||||
int format;
|
||||
if (span < MINUTE_IN_MILLIS) {
|
||||
span /= SECOND_IN_MILLIS;
|
||||
if (future) {
|
||||
format = context.getString(R.string.abbreviated_in_seconds);
|
||||
format = R.string.abbreviated_in_seconds;
|
||||
} else {
|
||||
format = context.getString(R.string.abbreviated_seconds_ago);
|
||||
format = R.string.abbreviated_seconds_ago;
|
||||
}
|
||||
} else if (span < HOUR) {
|
||||
span /= MINUTE;
|
||||
} else if (span < HOUR_IN_MILLIS) {
|
||||
span /= MINUTE_IN_MILLIS;
|
||||
if (future) {
|
||||
format = context.getString(R.string.abbreviated_in_minutes);
|
||||
format = R.string.abbreviated_in_minutes;
|
||||
} else {
|
||||
format = context.getString(R.string.abbreviated_minutes_ago);
|
||||
format = R.string.abbreviated_minutes_ago;
|
||||
}
|
||||
} else if (span < DAY) {
|
||||
span /= HOUR;
|
||||
} else if (span < DAY_IN_MILLIS) {
|
||||
span /= HOUR_IN_MILLIS;
|
||||
if (future) {
|
||||
format = context.getString(R.string.abbreviated_in_hours);
|
||||
format = R.string.abbreviated_in_hours;
|
||||
} else {
|
||||
format = context.getString(R.string.abbreviated_hours_ago);
|
||||
format = R.string.abbreviated_hours_ago;
|
||||
}
|
||||
} else if (span < YEAR) {
|
||||
span /= DAY;
|
||||
} else if (span < YEAR_IN_MILLIS) {
|
||||
span /= DAY_IN_MILLIS;
|
||||
if (future) {
|
||||
format = context.getString(R.string.abbreviated_in_days);
|
||||
format = R.string.abbreviated_in_days;
|
||||
} else {
|
||||
format = context.getString(R.string.abbreviated_days_ago);
|
||||
format = R.string.abbreviated_days_ago;
|
||||
}
|
||||
} else {
|
||||
span /= YEAR;
|
||||
span /= YEAR_IN_MILLIS;
|
||||
if (future) {
|
||||
format = context.getString(R.string.abbreviated_in_years);
|
||||
format = R.string.abbreviated_in_years;
|
||||
} 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
|
||||
))
|
||||
.setCollapsed(true)
|
||||
.setPoll(visibleStatus.getPoll())
|
||||
.setIsBot(visibleStatus.getAccount().getBot())
|
||||
|
||||
.createStatusViewData();
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,7 @@ import android.text.Spanned;
|
||||
import com.keylesspalace.tusky.entity.Attachment;
|
||||
import com.keylesspalace.tusky.entity.Card;
|
||||
import com.keylesspalace.tusky.entity.Emoji;
|
||||
import com.keylesspalace.tusky.entity.Poll;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -87,6 +88,8 @@ public abstract class StatusViewData {
|
||||
private final Card card;
|
||||
private final boolean isCollapsible; /** Whether the status meets the requirement to be collapse */
|
||||
final boolean isCollapsed; /** Whether the status is shown partially or fully */
|
||||
@Nullable
|
||||
private final Poll poll;
|
||||
private final boolean isBot;
|
||||
|
||||
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,
|
||||
@Nullable Status.Mention[] mentions, String senderId, boolean rebloggingEnabled,
|
||||
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;
|
||||
if (Build.VERSION.SDK_INT == 23) {
|
||||
// https://github.com/tuskyapp/Tusky/issues/563
|
||||
@ -132,6 +136,7 @@ public abstract class StatusViewData {
|
||||
this.card = card;
|
||||
this.isCollapsible = isCollapsible;
|
||||
this.isCollapsed = isCollapsed;
|
||||
this.poll = poll;
|
||||
this.isBot = isBot;
|
||||
}
|
||||
|
||||
@ -267,6 +272,11 @@ public abstract class StatusViewData {
|
||||
return isCollapsed;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Poll getPoll() {
|
||||
return poll;
|
||||
}
|
||||
|
||||
@Override public long getViewDataId() {
|
||||
// Chance of collision is super low and impact of mistake is low as well
|
||||
return id.hashCode();
|
||||
@ -302,7 +312,8 @@ public abstract class StatusViewData {
|
||||
Objects.equals(application, concrete.application) &&
|
||||
Objects.equals(statusEmojis, concrete.statusEmojis) &&
|
||||
Objects.equals(accountEmojis, concrete.accountEmojis) &&
|
||||
Objects.equals(card, concrete.card)
|
||||
Objects.equals(card, concrete.card) &&
|
||||
Objects.equals(poll, concrete.poll)
|
||||
&& isCollapsed == concrete.isCollapsed;
|
||||
}
|
||||
|
||||
@ -407,6 +418,7 @@ public abstract class StatusViewData {
|
||||
private Card card;
|
||||
private boolean isCollapsible; /** Whether the status meets the requirement to be collapsed */
|
||||
private boolean isCollapsed; /** Whether the status is shown partially or fully */
|
||||
private Poll poll;
|
||||
private boolean isBot;
|
||||
|
||||
public Builder() {
|
||||
@ -441,6 +453,7 @@ public abstract class StatusViewData {
|
||||
card = viewData.getCard();
|
||||
isCollapsible = viewData.isCollapsible();
|
||||
isCollapsed = viewData.isCollapsed();
|
||||
poll = viewData.poll;
|
||||
isBot = viewData.isBot();
|
||||
}
|
||||
|
||||
@ -603,6 +616,11 @@ public abstract class StatusViewData {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setPoll(Poll poll) {
|
||||
this.poll = poll;
|
||||
return this;
|
||||
}
|
||||
|
||||
public StatusViewData.Concrete createStatusViewData() {
|
||||
if (this.statusEmojis == null) statusEmojis = Collections.emptyList();
|
||||
if (this.accountEmojis == null) accountEmojis = Collections.emptyList();
|
||||
@ -612,7 +630,7 @@ public abstract class StatusViewData {
|
||||
attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded,
|
||||
isShowingContent, userFullName, nickname, avatar, createdAt, reblogsCount,
|
||||
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>
|
||||
|
||||
<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
|
||||
android:id="@+id/status_reply"
|
||||
style="?attr/image_button_style"
|
||||
@ -354,7 +497,7 @@
|
||||
app:layout_constraintEnd_toStartOf="@id/status_favourite"
|
||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||
app:layout_constraintTop_toBottomOf="@id/status_media_preview_container"
|
||||
app:layout_constraintTop_toBottomOf="@id/status_poll_description"
|
||||
app:srcCompat="@drawable/ic_reply_24dp" />
|
||||
|
||||
<at.connyduck.sparkbutton.SparkButton
|
||||
|
@ -327,6 +327,149 @@
|
||||
|
||||
</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
|
||||
android:id="@+id/status_reply"
|
||||
style="?attr/image_button_style"
|
||||
@ -341,7 +484,7 @@
|
||||
app:layout_constraintEnd_toStartOf="@id/status_inset"
|
||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||
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" />
|
||||
|
||||
<at.connyduck.sparkbutton.SparkButton
|
||||
|
@ -335,6 +335,151 @@
|
||||
|
||||
</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
|
||||
android:id="@+id/status_timestamp_info"
|
||||
android:layout_width="0dp"
|
||||
@ -346,7 +491,7 @@
|
||||
android:textSize="?attr/status_text_medium"
|
||||
app:layout_constraintLeft_toLeftOf="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" />
|
||||
|
||||
<View
|
||||
@ -355,8 +500,8 @@
|
||||
android:layout_height="1dp"
|
||||
android:layout_below="@id/status_timestamp_info"
|
||||
android:layout_marginTop="6dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:background="?android:attr/listDivider"
|
||||
android:importantForAccessibility="no"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/status_timestamp_info" />
|
||||
@ -403,8 +548,8 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:background="?android:attr/listDivider"
|
||||
android:importantForAccessibility="no"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/status_counters_barrier" />
|
||||
|
@ -78,6 +78,8 @@
|
||||
|
||||
<item name="minTouchTargetSize">32dp</item> <!-- this affects RadioButton size -->
|
||||
|
||||
<item name="pollOptionBackgroundColor">@color/color_primary_dark</item>
|
||||
|
||||
</style>
|
||||
|
||||
<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_large" format="dimension" />
|
||||
|
||||
<attr name="pollOptionBackgroundColor" format="reference|color" />
|
||||
|
||||
</resources>
|
@ -317,6 +317,12 @@
|
||||
<string name="abbreviated_minutes_ago">%dm</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="pref_title_alway_show_sensitive_media">Always show sensitive content</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="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>
|
||||
|
@ -140,6 +140,8 @@
|
||||
|
||||
<item name="minTouchTargetSize">32dp</item> <!-- this affects RadioButton size -->
|
||||
|
||||
<item name="pollOptionBackgroundColor">@color/color_primary_dark_light</item>
|
||||
|
||||
</style>
|
||||
|
||||
<style name="TuskyImageButton.Light" parent="@style/Widget.MaterialComponents.Button.UnelevatedButton">
|
||||
|
@ -84,7 +84,8 @@ class BottomSheetActivityTest {
|
||||
ArrayList(),
|
||||
arrayOf(),
|
||||
null,
|
||||
pinned = false
|
||||
pinned = false,
|
||||
poll = null
|
||||
)
|
||||
private val statusCallback = FakeSearchResults(status)
|
||||
|
||||
|
@ -305,7 +305,8 @@ class TimelineRepositoryTest {
|
||||
inReplyToId = null,
|
||||
pinned = false,
|
||||
reblog = null,
|
||||
url = "http://example.com/statuses/$id"
|
||||
url = "http://example.com/statuses/$id",
|
||||
poll = null
|
||||
)
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user