Merge remote-tracking branch 'tuskyapp/develop'

This commit is contained in:
kyori19 2022-12-06 02:25:11 +09:00
commit 5be9a90333
No known key found for this signature in database
GPG Key ID: F7BDE7DD42BF366A
163 changed files with 6934 additions and 1244 deletions

View File

@ -0,0 +1,959 @@
{
"formatVersion": 1,
"database": {
"version": 43,
"identityHash": "bf68abe55bb58765da7f9d6f7ef618e2",
"entities": [
{
"tableName": "DraftEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL, `scheduledAt` TEXT, `language` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "inReplyToId",
"columnName": "inReplyToId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "contentWarning",
"columnName": "contentWarning",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sensitive",
"columnName": "sensitive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "visibility",
"columnName": "visibility",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "attachments",
"columnName": "attachments",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "poll",
"columnName": "poll",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "failedToSend",
"columnName": "failedToSend",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "scheduledAt",
"columnName": "scheduledAt",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "language",
"columnName": "language",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "AccountEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `defaultPostLanguage` TEXT NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` 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": "clientId",
"columnName": "clientId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "clientSecret",
"columnName": "clientSecret",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isActive",
"columnName": "isActive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "profilePictureUrl",
"columnName": "profilePictureUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notificationsEnabled",
"columnName": "notificationsEnabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsMentioned",
"columnName": "notificationsMentioned",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsFollowed",
"columnName": "notificationsFollowed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsFollowRequested",
"columnName": "notificationsFollowRequested",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsReblogged",
"columnName": "notificationsReblogged",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsFavorited",
"columnName": "notificationsFavorited",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsPolls",
"columnName": "notificationsPolls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsSubscriptions",
"columnName": "notificationsSubscriptions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsSignUps",
"columnName": "notificationsSignUps",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsUpdates",
"columnName": "notificationsUpdates",
"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": "defaultPostLanguage",
"columnName": "defaultPostLanguage",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "alwaysShowSensitiveMedia",
"columnName": "alwaysShowSensitiveMedia",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "alwaysOpenSpoiler",
"columnName": "alwaysOpenSpoiler",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mediaPreviewEnabled",
"columnName": "mediaPreviewEnabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastNotificationId",
"columnName": "lastNotificationId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activeNotifications",
"columnName": "activeNotifications",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "emojis",
"columnName": "emojis",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tabPreferences",
"columnName": "tabPreferences",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notificationsFilter",
"columnName": "notificationsFilter",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "oauthScopes",
"columnName": "oauthScopes",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unifiedPushUrl",
"columnName": "unifiedPushUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "pushPubKey",
"columnName": "pushPubKey",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "pushPrivKey",
"columnName": "pushPrivKey",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "pushAuth",
"columnName": "pushAuth",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "pushServerKey",
"columnName": "pushServerKey",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_AccountEntity_domain_accountId",
"unique": true,
"columnNames": [
"domain",
"accountId"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)"
}
],
"foreignKeys": []
},
{
"tableName": "InstanceEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` 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
},
{
"fieldPath": "maxPollOptions",
"columnName": "maxPollOptions",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxPollOptionLength",
"columnName": "maxPollOptionLength",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "minPollDuration",
"columnName": "minPollDuration",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxPollDuration",
"columnName": "maxPollDuration",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "charactersReservedPerUrl",
"columnName": "charactersReservedPerUrl",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "version",
"columnName": "version",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "videoSizeLimit",
"columnName": "videoSizeLimit",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "imageSizeLimit",
"columnName": "imageSizeLimit",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "imageMatrixLimit",
"columnName": "imageMatrixLimit",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxMediaAttachments",
"columnName": "maxMediaAttachments",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxFields",
"columnName": "maxFields",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxFieldNameLength",
"columnName": "maxFieldNameLength",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxFieldValueLength",
"columnName": "maxFieldValueLength",
"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, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, `language` 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": "repliesCount",
"columnName": "repliesCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "reblogged",
"columnName": "reblogged",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bookmarked",
"columnName": "bookmarked",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favourited",
"columnName": "favourited",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sensitive",
"columnName": "sensitive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "spoilerText",
"columnName": "spoilerText",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "visibility",
"columnName": "visibility",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "attachments",
"columnName": "attachments",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "mentions",
"columnName": "mentions",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "application",
"columnName": "application",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reblogServerId",
"columnName": "reblogServerId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reblogAccountId",
"columnName": "reblogAccountId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "poll",
"columnName": "poll",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "muted",
"columnName": "muted",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "expanded",
"columnName": "expanded",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "contentCollapsed",
"columnName": "contentCollapsed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "contentShowing",
"columnName": "contentShowing",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pinned",
"columnName": "pinned",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "card",
"columnName": "card",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "language",
"columnName": "language",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"serverId",
"timelineUserId"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
"unique": false,
"columnNames": [
"authorServerId",
"timelineUserId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)"
}
],
"foreignKeys": [
{
"table": "TimelineAccountEntity",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"authorServerId",
"timelineUserId"
],
"referencedColumns": [
"serverId",
"timelineUserId"
]
}
]
},
{
"tableName": "TimelineAccountEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))",
"fields": [
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timelineUserId",
"columnName": "timelineUserId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "localUsername",
"columnName": "localUsername",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "avatar",
"columnName": "avatar",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "emojis",
"columnName": "emojis",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "bot",
"columnName": "bot",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"serverId",
"timelineUserId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "ConversationEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER 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_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, `s_language` TEXT, PRIMARY KEY(`id`, `accountId`))",
"fields": [
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "order",
"columnName": "order",
"affinity": "INTEGER",
"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.repliesCount",
"columnName": "s_repliesCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.favourited",
"columnName": "s_favourited",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.bookmarked",
"columnName": "s_bookmarked",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.sensitive",
"columnName": "s_sensitive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.spoilerText",
"columnName": "s_spoilerText",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.attachments",
"columnName": "s_attachments",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.mentions",
"columnName": "s_mentions",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.tags",
"columnName": "s_tags",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastStatus.showingHiddenContent",
"columnName": "s_showingHiddenContent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.expanded",
"columnName": "s_expanded",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.collapsed",
"columnName": "s_collapsed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.muted",
"columnName": "s_muted",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.poll",
"columnName": "s_poll",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastStatus.language",
"columnName": "s_language",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id",
"accountId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bf68abe55bb58765da7f9d6f7ef618e2')"
]
}
}

View File

@ -0,0 +1,965 @@
{
"formatVersion": 1,
"database": {
"version": 44,
"identityHash": "7b5271980102f35e55438f46777e3d46",
"entities": [
{
"tableName": "DraftEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL, `scheduledAt` TEXT, `language` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "inReplyToId",
"columnName": "inReplyToId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "contentWarning",
"columnName": "contentWarning",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sensitive",
"columnName": "sensitive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "visibility",
"columnName": "visibility",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "attachments",
"columnName": "attachments",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "poll",
"columnName": "poll",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "failedToSend",
"columnName": "failedToSend",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "scheduledAt",
"columnName": "scheduledAt",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "language",
"columnName": "language",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "AccountEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationsReports` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `defaultPostLanguage` TEXT NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` 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": "clientId",
"columnName": "clientId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "clientSecret",
"columnName": "clientSecret",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isActive",
"columnName": "isActive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "profilePictureUrl",
"columnName": "profilePictureUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notificationsEnabled",
"columnName": "notificationsEnabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsMentioned",
"columnName": "notificationsMentioned",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsFollowed",
"columnName": "notificationsFollowed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsFollowRequested",
"columnName": "notificationsFollowRequested",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsReblogged",
"columnName": "notificationsReblogged",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsFavorited",
"columnName": "notificationsFavorited",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsPolls",
"columnName": "notificationsPolls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsSubscriptions",
"columnName": "notificationsSubscriptions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsSignUps",
"columnName": "notificationsSignUps",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsUpdates",
"columnName": "notificationsUpdates",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsReports",
"columnName": "notificationsReports",
"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": "defaultPostLanguage",
"columnName": "defaultPostLanguage",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "alwaysShowSensitiveMedia",
"columnName": "alwaysShowSensitiveMedia",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "alwaysOpenSpoiler",
"columnName": "alwaysOpenSpoiler",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mediaPreviewEnabled",
"columnName": "mediaPreviewEnabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastNotificationId",
"columnName": "lastNotificationId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activeNotifications",
"columnName": "activeNotifications",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "emojis",
"columnName": "emojis",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tabPreferences",
"columnName": "tabPreferences",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notificationsFilter",
"columnName": "notificationsFilter",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "oauthScopes",
"columnName": "oauthScopes",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unifiedPushUrl",
"columnName": "unifiedPushUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "pushPubKey",
"columnName": "pushPubKey",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "pushPrivKey",
"columnName": "pushPrivKey",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "pushAuth",
"columnName": "pushAuth",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "pushServerKey",
"columnName": "pushServerKey",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_AccountEntity_domain_accountId",
"unique": true,
"columnNames": [
"domain",
"accountId"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)"
}
],
"foreignKeys": []
},
{
"tableName": "InstanceEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` 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
},
{
"fieldPath": "maxPollOptions",
"columnName": "maxPollOptions",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxPollOptionLength",
"columnName": "maxPollOptionLength",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "minPollDuration",
"columnName": "minPollDuration",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxPollDuration",
"columnName": "maxPollDuration",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "charactersReservedPerUrl",
"columnName": "charactersReservedPerUrl",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "version",
"columnName": "version",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "videoSizeLimit",
"columnName": "videoSizeLimit",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "imageSizeLimit",
"columnName": "imageSizeLimit",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "imageMatrixLimit",
"columnName": "imageMatrixLimit",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxMediaAttachments",
"columnName": "maxMediaAttachments",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxFields",
"columnName": "maxFields",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxFieldNameLength",
"columnName": "maxFieldNameLength",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxFieldValueLength",
"columnName": "maxFieldValueLength",
"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, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, `language` 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": "repliesCount",
"columnName": "repliesCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "reblogged",
"columnName": "reblogged",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bookmarked",
"columnName": "bookmarked",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favourited",
"columnName": "favourited",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sensitive",
"columnName": "sensitive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "spoilerText",
"columnName": "spoilerText",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "visibility",
"columnName": "visibility",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "attachments",
"columnName": "attachments",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "mentions",
"columnName": "mentions",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "application",
"columnName": "application",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reblogServerId",
"columnName": "reblogServerId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reblogAccountId",
"columnName": "reblogAccountId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "poll",
"columnName": "poll",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "muted",
"columnName": "muted",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "expanded",
"columnName": "expanded",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "contentCollapsed",
"columnName": "contentCollapsed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "contentShowing",
"columnName": "contentShowing",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pinned",
"columnName": "pinned",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "card",
"columnName": "card",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "language",
"columnName": "language",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"serverId",
"timelineUserId"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
"unique": false,
"columnNames": [
"authorServerId",
"timelineUserId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)"
}
],
"foreignKeys": [
{
"table": "TimelineAccountEntity",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"authorServerId",
"timelineUserId"
],
"referencedColumns": [
"serverId",
"timelineUserId"
]
}
]
},
{
"tableName": "TimelineAccountEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))",
"fields": [
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timelineUserId",
"columnName": "timelineUserId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "localUsername",
"columnName": "localUsername",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "avatar",
"columnName": "avatar",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "emojis",
"columnName": "emojis",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "bot",
"columnName": "bot",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"serverId",
"timelineUserId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "ConversationEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER 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_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, `s_language` TEXT, PRIMARY KEY(`id`, `accountId`))",
"fields": [
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "order",
"columnName": "order",
"affinity": "INTEGER",
"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.repliesCount",
"columnName": "s_repliesCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.favourited",
"columnName": "s_favourited",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.bookmarked",
"columnName": "s_bookmarked",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.sensitive",
"columnName": "s_sensitive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.spoilerText",
"columnName": "s_spoilerText",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.attachments",
"columnName": "s_attachments",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.mentions",
"columnName": "s_mentions",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.tags",
"columnName": "s_tags",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastStatus.showingHiddenContent",
"columnName": "s_showingHiddenContent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.expanded",
"columnName": "s_expanded",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.collapsed",
"columnName": "s_collapsed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.muted",
"columnName": "s_muted",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.poll",
"columnName": "s_poll",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastStatus.language",
"columnName": "s_language",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id",
"accountId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7b5271980102f35e55438f46777e3d46')"
]
}
}

View File

@ -0,0 +1,983 @@
{
"formatVersion": 1,
"database": {
"version": 45,
"identityHash": "edb371b819690636d843eebffa55792a",
"entities": [
{
"tableName": "DraftEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL, `scheduledAt` TEXT, `language` TEXT)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "inReplyToId",
"columnName": "inReplyToId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "contentWarning",
"columnName": "contentWarning",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sensitive",
"columnName": "sensitive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "visibility",
"columnName": "visibility",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "attachments",
"columnName": "attachments",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "poll",
"columnName": "poll",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "failedToSend",
"columnName": "failedToSend",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "scheduledAt",
"columnName": "scheduledAt",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "language",
"columnName": "language",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "AccountEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationsReports` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `defaultPostLanguage` TEXT NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` 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": "clientId",
"columnName": "clientId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "clientSecret",
"columnName": "clientSecret",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "isActive",
"columnName": "isActive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "profilePictureUrl",
"columnName": "profilePictureUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notificationsEnabled",
"columnName": "notificationsEnabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsMentioned",
"columnName": "notificationsMentioned",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsFollowed",
"columnName": "notificationsFollowed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsFollowRequested",
"columnName": "notificationsFollowRequested",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsReblogged",
"columnName": "notificationsReblogged",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsFavorited",
"columnName": "notificationsFavorited",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsPolls",
"columnName": "notificationsPolls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsSubscriptions",
"columnName": "notificationsSubscriptions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsSignUps",
"columnName": "notificationsSignUps",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsUpdates",
"columnName": "notificationsUpdates",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsReports",
"columnName": "notificationsReports",
"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": "defaultPostLanguage",
"columnName": "defaultPostLanguage",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "alwaysShowSensitiveMedia",
"columnName": "alwaysShowSensitiveMedia",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "alwaysOpenSpoiler",
"columnName": "alwaysOpenSpoiler",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mediaPreviewEnabled",
"columnName": "mediaPreviewEnabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastNotificationId",
"columnName": "lastNotificationId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activeNotifications",
"columnName": "activeNotifications",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "emojis",
"columnName": "emojis",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tabPreferences",
"columnName": "tabPreferences",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notificationsFilter",
"columnName": "notificationsFilter",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "oauthScopes",
"columnName": "oauthScopes",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unifiedPushUrl",
"columnName": "unifiedPushUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "pushPubKey",
"columnName": "pushPubKey",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "pushPrivKey",
"columnName": "pushPrivKey",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "pushAuth",
"columnName": "pushAuth",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "pushServerKey",
"columnName": "pushServerKey",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_AccountEntity_domain_accountId",
"unique": true,
"columnNames": [
"domain",
"accountId"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)"
}
],
"foreignKeys": []
},
{
"tableName": "InstanceEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` 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
},
{
"fieldPath": "maxPollOptions",
"columnName": "maxPollOptions",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxPollOptionLength",
"columnName": "maxPollOptionLength",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "minPollDuration",
"columnName": "minPollDuration",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxPollDuration",
"columnName": "maxPollDuration",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "charactersReservedPerUrl",
"columnName": "charactersReservedPerUrl",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "version",
"columnName": "version",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "videoSizeLimit",
"columnName": "videoSizeLimit",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "imageSizeLimit",
"columnName": "imageSizeLimit",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "imageMatrixLimit",
"columnName": "imageMatrixLimit",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxMediaAttachments",
"columnName": "maxMediaAttachments",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxFields",
"columnName": "maxFields",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxFieldNameLength",
"columnName": "maxFieldNameLength",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxFieldValueLength",
"columnName": "maxFieldValueLength",
"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, `editedAt` INTEGER, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, `language` TEXT, `quote` 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": "editedAt",
"columnName": "editedAt",
"affinity": "INTEGER",
"notNull": false
},
{
"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": "repliesCount",
"columnName": "repliesCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "reblogged",
"columnName": "reblogged",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bookmarked",
"columnName": "bookmarked",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favourited",
"columnName": "favourited",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sensitive",
"columnName": "sensitive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "spoilerText",
"columnName": "spoilerText",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "visibility",
"columnName": "visibility",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "attachments",
"columnName": "attachments",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "mentions",
"columnName": "mentions",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "tags",
"columnName": "tags",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "application",
"columnName": "application",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reblogServerId",
"columnName": "reblogServerId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reblogAccountId",
"columnName": "reblogAccountId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "poll",
"columnName": "poll",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "muted",
"columnName": "muted",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "expanded",
"columnName": "expanded",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "contentCollapsed",
"columnName": "contentCollapsed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "contentShowing",
"columnName": "contentShowing",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pinned",
"columnName": "pinned",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "card",
"columnName": "card",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "language",
"columnName": "language",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "quote",
"columnName": "quote",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"serverId",
"timelineUserId"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
"unique": false,
"columnNames": [
"authorServerId",
"timelineUserId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)"
}
],
"foreignKeys": [
{
"table": "TimelineAccountEntity",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"authorServerId",
"timelineUserId"
],
"referencedColumns": [
"serverId",
"timelineUserId"
]
}
]
},
{
"tableName": "TimelineAccountEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))",
"fields": [
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timelineUserId",
"columnName": "timelineUserId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "localUsername",
"columnName": "localUsername",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "avatar",
"columnName": "avatar",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "emojis",
"columnName": "emojis",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "bot",
"columnName": "bot",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"serverId",
"timelineUserId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "ConversationEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER 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_editedAt` INTEGER, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, `s_language` TEXT, PRIMARY KEY(`id`, `accountId`))",
"fields": [
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "order",
"columnName": "order",
"affinity": "INTEGER",
"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.editedAt",
"columnName": "s_editedAt",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "lastStatus.emojis",
"columnName": "s_emojis",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.favouritesCount",
"columnName": "s_favouritesCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.repliesCount",
"columnName": "s_repliesCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.favourited",
"columnName": "s_favourited",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.bookmarked",
"columnName": "s_bookmarked",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.sensitive",
"columnName": "s_sensitive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.spoilerText",
"columnName": "s_spoilerText",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.attachments",
"columnName": "s_attachments",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.mentions",
"columnName": "s_mentions",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.tags",
"columnName": "s_tags",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastStatus.showingHiddenContent",
"columnName": "s_showingHiddenContent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.expanded",
"columnName": "s_expanded",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.collapsed",
"columnName": "s_collapsed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.muted",
"columnName": "s_muted",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.poll",
"columnName": "s_poll",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastStatus.language",
"columnName": "s_language",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id",
"accountId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'edb371b819690636d843eebffa55792a')"
]
}
}

View File

@ -6,8 +6,8 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
<uses-permission android:name="android.permission.VIBRATE" /> <!-- For notifications -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
@ -19,7 +19,8 @@
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/TuskyTheme"
android:usesCleartextTraffic="false">
android:usesCleartextTraffic="false"
android:localeConfig="@xml/locales_config">
<activity
android:name=".SplashActivity"
@ -133,6 +134,7 @@
<activity android:name=".ListsActivity" />
<activity android:name=".LicenseActivity" />
<activity android:name=".FiltersActivity" />
<activity android:name=".components.followedtags.FollowedTagsActivity" />
<activity
android:name=".components.report.ReportActivity"
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />

View File

@ -16,7 +16,6 @@
package com.keylesspalace.tusky;
import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
@ -92,11 +91,6 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
requesters = new HashMap<>();
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(TuskyApplication.getLocaleManager().setLocale(base));
}
protected boolean requiresLogin() {
return true;
}

View File

@ -29,10 +29,9 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.keylesspalace.tusky.components.account.AccountActivity
import com.keylesspalace.tusky.components.viewthread.ViewThreadActivity
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.looksLikeMastodonUrl
import com.keylesspalace.tusky.util.openLink
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import java.net.URI
import java.net.URISyntaxException
import javax.inject.Inject
/** this is the base class for all activities that open links
@ -180,47 +179,6 @@ abstract class BottomSheetActivity : BaseActivity() {
}
}
// https://mastodon.foo.bar/@User
// https://mastodon.foo.bar/@User/43456787654678
// https://pleroma.foo.bar/users/User
// https://pleroma.foo.bar/users/9qTHT2ANWUdXzENqC0
// https://pleroma.foo.bar/notice/9sBHWIlwwGZi5QGlHc
// https://pleroma.foo.bar/objects/d4643c42-3ae0-4b73-b8b0-c725f5819207
// https://friendica.foo.bar/profile/user
// https://friendica.foo.bar/display/d4643c42-3ae0-4b73-b8b0-c725f5819207
// https://misskey.foo.bar/notes/83w6r388br (always lowercase)
// https://pixelfed.social/p/connyduck/391263492998670833
// https://pixelfed.social/connyduck
// https://mastodon.foo.bar/users/User/statuses/000000000000000000
fun looksLikeMastodonUrl(urlString: String): Boolean {
val uri: URI
try {
uri = URI(urlString)
} catch (e: URISyntaxException) {
return false
}
if (uri.query != null ||
uri.fragment != null ||
uri.path == null
) {
return false
}
val path = uri.path
return path.matches("^/@[^/]+$".toRegex()) ||
path.matches("^/@[^/]+/\\d+$".toRegex()) ||
path.matches("^/users/\\w+$".toRegex()) ||
path.matches("^/notice/[a-zA-Z0-9]+$".toRegex()) ||
path.matches("^/objects/[-a-f0-9]+$".toRegex()) ||
path.matches("^/notes/[a-z0-9]+$".toRegex()) ||
path.matches("^/display/[-a-f0-9]+$".toRegex()) ||
path.matches("^/profile/\\w+$".toRegex()) ||
path.matches("^/p/\\w+/\\d+$".toRegex()) ||
path.matches("^/\\w+$".toRegex()) ||
path.matches("^/users/[^/]+/statuses/\\d+$".toRegex())
}
enum class PostLookupFallbackBehavior {
OPEN_IN_BROWSER,
DISPLAY_ERROR,

View File

@ -434,9 +434,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
private fun setupDrawer(savedInstanceState: Bundle?, addSearchButton: Boolean) {
binding.mainToolbar.setNavigationOnClickListener {
binding.mainDrawerLayout.open()
}
val drawerOpenClickListener = View.OnClickListener { binding.mainDrawerLayout.open() }
binding.mainToolbar.setNavigationOnClickListener(drawerOpenClickListener)
binding.topNavAvatar.setOnClickListener(drawerOpenClickListener)
binding.bottomNavAvatar.setOnClickListener(drawerOpenClickListener)
header = AccountHeaderView(this).apply {
headerBackgroundScaleType = ImageView.ScaleType.CENTER_CROP
@ -648,7 +650,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
val actionBarSize = ThemeUtils.getDimension(this, R.attr.actionBarSize)
val fabMargin = resources.getDimensionPixelSize(R.dimen.fabMargin)
(binding.composeButton.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = actionBarSize + fabMargin
binding.tabLayout.hide()
binding.topNav.hide()
binding.bottomTabLayout
} else {
binding.bottomNav.hide()
@ -784,7 +786,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
val pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin)
binding.viewPager.setPageTransformer(MarginPageTransformer(pageMargin))
val enableSwipeForTabs = preferences.getBoolean("enableSwipeForTabs", true)
val enableSwipeForTabs = preferences.getBoolean(PrefKeys.ENABLE_SWIPE_FOR_TABS, true)
binding.viewPager.isUserInputEnabled = enableSwipeForTabs
onTabSelectedListener?.let {
@ -922,71 +924,117 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
private fun loadDrawerAvatar(avatarUrl: String, showPlaceholder: Boolean) {
val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size)
val hideTopToolbar = preferences.getBoolean(PrefKeys.HIDE_TOP_TOOLBAR, false)
val animateAvatars = preferences.getBoolean("animateGifAvatars", false)
if (animateAvatars) {
glide.asDrawable()
.load(avatarUrl)
.transform(
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
)
.apply {
if (showPlaceholder) {
placeholder(R.drawable.avatar_default)
}
}
.into(object : CustomTarget<Drawable>(navIconSize, navIconSize) {
if (hideTopToolbar) {
val navOnBottom = preferences.getString("mainNavPosition", "top") == "bottom"
override fun onLoadStarted(placeholder: Drawable?) {
if (placeholder != null) {
binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
}
}
val avatarView = if (navOnBottom) {
binding.bottomNavAvatar.show()
binding.bottomNavAvatar
} else {
binding.topNavAvatar.show()
binding.topNavAvatar
}
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
if (resource is Animatable) {
resource.start()
}
binding.mainToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize)
}
override fun onLoadCleared(placeholder: Drawable?) {
if (placeholder != null) {
binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
}
}
})
if (animateAvatars) {
Glide.with(this)
.load(avatarUrl)
.placeholder(R.drawable.avatar_default)
.into(avatarView)
} else {
Glide.with(this)
.asBitmap()
.load(avatarUrl)
.placeholder(R.drawable.avatar_default)
.into(avatarView)
}
} else {
glide.asBitmap()
.load(avatarUrl)
.transform(
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
)
.apply {
if (showPlaceholder) {
placeholder(R.drawable.avatar_default)
}
}
.into(object : CustomTarget<Bitmap>(navIconSize, navIconSize) {
override fun onLoadStarted(placeholder: Drawable?) {
if (placeholder != null) {
binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
binding.bottomNavAvatar.hide()
binding.topNavAvatar.hide()
val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size)
if (animateAvatars) {
glide.asDrawable()
.load(avatarUrl)
.transform(
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
)
.apply {
if (showPlaceholder) {
placeholder(R.drawable.avatar_default)
}
}
.into(object : CustomTarget<Drawable>(navIconSize, navIconSize) {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
binding.mainToolbar.navigationIcon = FixedSizeDrawable(BitmapDrawable(resources, resource), navIconSize, navIconSize)
}
override fun onLoadStarted(placeholder: Drawable?) {
if (placeholder != null) {
binding.mainToolbar.navigationIcon =
FixedSizeDrawable(placeholder, navIconSize, navIconSize)
}
}
override fun onLoadCleared(placeholder: Drawable?) {
if (placeholder != null) {
binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
override fun onResourceReady(
resource: Drawable,
transition: Transition<in Drawable>?
) {
if (resource is Animatable) {
resource.start()
}
binding.mainToolbar.navigationIcon =
FixedSizeDrawable(resource, navIconSize, navIconSize)
}
override fun onLoadCleared(placeholder: Drawable?) {
if (placeholder != null) {
binding.mainToolbar.navigationIcon =
FixedSizeDrawable(placeholder, navIconSize, navIconSize)
}
}
})
} else {
glide.asBitmap()
.load(avatarUrl)
.transform(
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
)
.apply {
if (showPlaceholder) {
placeholder(R.drawable.avatar_default)
}
}
})
.into(object : CustomTarget<Bitmap>(navIconSize, navIconSize) {
override fun onLoadStarted(placeholder: Drawable?) {
if (placeholder != null) {
binding.mainToolbar.navigationIcon =
FixedSizeDrawable(placeholder, navIconSize, navIconSize)
}
}
override fun onResourceReady(
resource: Bitmap,
transition: Transition<in Bitmap>?
) {
binding.mainToolbar.navigationIcon = FixedSizeDrawable(
BitmapDrawable(resources, resource),
navIconSize,
navIconSize
)
}
override fun onLoadCleared(placeholder: Drawable?) {
if (placeholder != null) {
binding.mainToolbar.navigationIcon =
FixedSizeDrawable(placeholder, navIconSize, navIconSize)
}
}
})
}
}
}

View File

@ -26,13 +26,16 @@ import androidx.fragment.app.commit
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import at.connyduck.calladapter.networkresult.fold
import autodispose2.androidx.lifecycle.autoDispose
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider
import autodispose2.autoDispose
import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
import com.keylesspalace.tusky.components.timeline.TimelineFragment
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel.Kind
import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.util.viewBinding
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
@ -52,13 +55,20 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
private val quickTootViewModel: QuickTootViewModel by viewModels { viewModelFactory }
private val binding: ActivityStatuslistBinding by viewBinding(ActivityStatuslistBinding::inflate)
private lateinit var kind: Kind
private var hashtag: String? = null
private var followTagItem: MenuItem? = null
private var unfollowTagItem: MenuItem? = null
private var muteTagItem: MenuItem? = null
private var unmuteTagItem: MenuItem? = null
/** The filter muting hashtag, null if unknown or hashtag is not filtered */
private var mutedFilter: Filter? = null
override fun onCreate(savedInstanceState: Bundle?) {
Log.d("StatusListActivity", "onCreate")
super.onCreate(savedInstanceState)
setContentView(binding.root)
@ -96,7 +106,7 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
eventHub.events
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
.autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
.subscribe(binding.viewQuickToot::handleEvent)
binding.floatingBtn.setOnClickListener(binding.viewQuickToot::onFABClicked)
}
@ -110,10 +120,15 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
menuInflater.inflate(R.menu.view_hashtag_toolbar, menu)
followTagItem = menu.findItem(R.id.action_follow_hashtag)
unfollowTagItem = menu.findItem(R.id.action_unfollow_hashtag)
muteTagItem = menu.findItem(R.id.action_mute_hashtag)
unmuteTagItem = menu.findItem(R.id.action_unmute_hashtag)
followTagItem?.isVisible = tagEntity.following == false
unfollowTagItem?.isVisible = tagEntity.following == true
followTagItem?.setOnMenuItemClickListener { followTag() }
unfollowTagItem?.setOnMenuItemClickListener { unfollowTag() }
muteTagItem?.setOnMenuItemClickListener { muteTag() }
unmuteTagItem?.setOnMenuItemClickListener { unmuteTag() }
updateMuteTagMenuItems()
},
{
Log.w(TAG, "Failed to query tag #$tag", it)
@ -165,6 +180,85 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
return true
}
/**
* Determine if the current hashtag is muted, and update the UI state accordingly.
*/
private fun updateMuteTagMenuItems() {
val tag = hashtag ?: return
muteTagItem?.isVisible = true
muteTagItem?.isEnabled = false
unmuteTagItem?.isVisible = false
mastodonApi.getFilters().observeOn(AndroidSchedulers.mainThread())
.autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
.subscribe { filters ->
for (filter in filters) {
if ((tag == filter.phrase) and filter.context.contains(Filter.HOME)) {
Log.d(TAG, "Tag $hashtag is filtered")
muteTagItem?.isVisible = false
unmuteTagItem?.isVisible = true
mutedFilter = filter
return@subscribe
}
}
Log.d(TAG, "Tag $hashtag is not filtered")
mutedFilter = null
muteTagItem?.isEnabled = true
muteTagItem?.isVisible = true
muteTagItem?.isVisible = true
}
}
private fun muteTag(): Boolean {
val tag = hashtag ?: return true
lifecycleScope.launch {
mastodonApi.createFilter(
tag,
listOf(Filter.HOME),
irreversible = false,
wholeWord = true,
expiresInSeconds = null
).fold(
{ filter ->
mutedFilter = filter
muteTagItem?.isVisible = false
unmuteTagItem?.isVisible = true
eventHub.dispatch(PreferenceChangedEvent(filter.context[0]))
},
{
Snackbar.make(binding.root, getString(R.string.error_muting_hashtag_format, tag), Snackbar.LENGTH_SHORT).show()
Log.e(TAG, "Failed to mute #$tag", it)
}
)
}
return true
}
private fun unmuteTag(): Boolean {
val filter = mutedFilter ?: return true
lifecycleScope.launch {
mastodonApi.deleteFilter(filter.id).fold(
{
muteTagItem?.isVisible = true
unmuteTagItem?.isVisible = false
eventHub.dispatch(PreferenceChangedEvent(filter.context[0]))
mutedFilter = null
},
{
Snackbar.make(binding.root, getString(R.string.error_unmuting_hashtag_format, filter.phrase), Snackbar.LENGTH_SHORT).show()
Log.e(TAG, "Failed to unmute #${filter.phrase}", it)
}
)
}
return true
}
override fun androidInjector() = dispatchingAndroidInjector
companion object {

View File

@ -108,6 +108,6 @@ fun defaultTabs(): List<TabData> {
createTabDataFromId(HOME),
createTabDataFromId(NOTIFICATIONS),
createTabDataFromId(LOCAL),
createTabDataFromId(FEDERATED)
createTabDataFromId(DIRECT)
)
}

View File

@ -16,8 +16,6 @@
package com.keylesspalace.tusky
import android.app.Application
import android.content.Context
import android.content.res.Configuration
import android.util.Log
import androidx.preference.PreferenceManager
import androidx.work.WorkManager
@ -44,6 +42,9 @@ class TuskyApplication : Application(), HasAndroidInjector {
@Inject
lateinit var notificationWorkerFactory: NotificationWorkerFactory
@Inject
lateinit var localeManager: LocaleManager
override fun onCreate() {
// Uncomment me to get StrictMode violation logs
// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
@ -74,6 +75,8 @@ class TuskyApplication : Application(), HasAndroidInjector {
val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
ThemeUtils.setAppNightMode(theme)
localeManager.setLocale()
RxJavaPlugins.setErrorHandler {
Log.w("RxJava", "undeliverable exception", it)
}
@ -86,20 +89,5 @@ class TuskyApplication : Application(), HasAndroidInjector {
)
}
override fun attachBaseContext(base: Context) {
localeManager = LocaleManager(base)
super.attachBaseContext(localeManager.setLocale(base))
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
localeManager.setLocale(this)
}
override fun androidInjector() = androidInjector
companion object {
@JvmStatic
lateinit var localeManager: LocaleManager
}
}

View File

@ -27,6 +27,7 @@ import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.transition.Transition
@ -51,6 +52,7 @@ import com.keylesspalace.tusky.components.viewthread.ViewThreadActivity
import com.keylesspalace.tusky.databinding.ActivityViewMediaBinding
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.fragment.ViewImageFragment
import com.keylesspalace.tusky.fragment.ViewVideoFragment
import com.keylesspalace.tusky.pager.ImagePagerAdapter
import com.keylesspalace.tusky.pager.SingleImagePagerAdapter
import com.keylesspalace.tusky.util.getTemporaryMediaFilename
@ -67,7 +69,7 @@ import java.util.Locale
typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit
class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener {
class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener, ViewVideoFragment.VideoActionsListener {
private val binding by viewBinding(ActivityViewMediaBinding::inflate)
@ -212,12 +214,20 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
}
private fun requestDownloadMedia() {
requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { _, grantResults ->
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
downloadMedia()
} else {
showErrorDialog(binding.toolbar, R.string.error_media_download_permission, R.string.action_retry) { requestDownloadMedia() }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { _, grantResults ->
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
downloadMedia()
} else {
showErrorDialog(
binding.toolbar,
R.string.error_media_download_permission,
R.string.action_retry
) { requestDownloadMedia() }
}
}
} else {
downloadMedia()
}
}

View File

@ -17,6 +17,7 @@ package com.keylesspalace.tusky.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.widget.TooltipCompat
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.keylesspalace.tusky.databinding.ItemEmojiButtonBinding
@ -52,6 +53,7 @@ class EmojiAdapter(
}
emojiImageView.contentDescription = emoji.shortcode
TooltipCompat.setTooltipText(emojiImageView, emoji.shortcode)
}
}

View File

@ -22,6 +22,8 @@ import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.getTuskyDisplayName
import com.keylesspalace.tusky.util.modernLanguageCode
import java.util.Locale
class LocaleAdapter(context: Context, resource: Int, locales: List<Locale>) : ArrayAdapter<Locale>(context, resource, locales) {
@ -29,15 +31,14 @@ class LocaleAdapter(context: Context, resource: Int, locales: List<Locale>) : Ar
return (super.getView(position, convertView, parent) as TextView).apply {
setTextColor(ThemeUtils.getColor(context, android.R.attr.textColorTertiary))
typeface = Typeface.DEFAULT_BOLD
text = super.getItem(position)?.language?.uppercase()
text = super.getItem(position)?.modernLanguageCode?.uppercase()
}
}
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
return (super.getDropDownView(position, convertView, parent) as TextView).apply {
setTextColor(ThemeUtils.getColor(context, android.R.attr.textColorTertiary))
val locale = super.getItem(position)
text = "${locale?.displayLanguage} (${locale?.getDisplayLanguage(locale)})"
text = super.getItem(position)?.getTuskyDisplayName(context)
}
}
}

View File

@ -43,6 +43,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding;
import com.keylesspalace.tusky.databinding.ItemReportNotificationBinding;
import com.keylesspalace.tusky.databinding.ViewQuoteInlineBinding;
import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.entity.Notification;
@ -84,7 +85,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
private static final int VIEW_TYPE_FOLLOW = 2;
private static final int VIEW_TYPE_FOLLOW_REQUEST = 3;
private static final int VIEW_TYPE_PLACEHOLDER = 4;
private static final int VIEW_TYPE_UNKNOWN = 5;
private static final int VIEW_TYPE_REPORT = 5;
private static final int VIEW_TYPE_UNKNOWN = 6;
private static final InputFilter[] COLLAPSE_INPUT_FILTER = new InputFilter[]{SmartLengthInputFilter.INSTANCE};
private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0];
@ -141,6 +143,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
.inflate(R.layout.item_status_placeholder, parent, false);
return new PlaceholderViewHolder(view);
}
case VIEW_TYPE_REPORT: {
ItemReportNotificationBinding binding = ItemReportNotificationBinding.inflate(inflater, parent, false);
return new ReportNotificationViewHolder(binding);
}
default:
case VIEW_TYPE_UNKNOWN: {
View view = new View(parent.getContext());
@ -256,6 +262,13 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
}
break;
}
case VIEW_TYPE_REPORT: {
if (payloadForHolder == null) {
ReportNotificationViewHolder holder = (ReportNotificationViewHolder) viewHolder;
holder.setupWithReport(concreteNotification.getAccount(), concreteNotification.getReport(), statusDisplayOptions.animateAvatars(), statusDisplayOptions.animateEmojis());
holder.setupActionListener(notificationActionListener, concreteNotification.getReport().getTargetAccount().getId(), concreteNotification.getAccount().getId(), concreteNotification.getReport().getId());
}
}
default:
}
}
@ -309,6 +322,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
case FOLLOW_REQUEST: {
return VIEW_TYPE_FOLLOW_REQUEST;
}
case REPORT: {
return VIEW_TYPE_REPORT;
}
default: {
return VIEW_TYPE_UNKNOWN;
}
@ -327,6 +343,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
void onViewStatusForNotificationId(String notificationId);
void onViewReport(String reportId);
void onExpandedChange(boolean expanded, int position);
/**

View File

@ -0,0 +1,90 @@
/* Copyright 2021 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.adapter
import android.content.Context
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import at.connyduck.sparkbutton.helpers.Utils
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.adapter.NotificationsAdapter.NotificationActionListener
import com.keylesspalace.tusky.databinding.ItemReportNotificationBinding
import com.keylesspalace.tusky.entity.Report
import com.keylesspalace.tusky.entity.TimelineAccount
import com.keylesspalace.tusky.util.TimestampUtils
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.loadAvatar
import com.keylesspalace.tusky.util.unicodeWrap
import java.util.Date
class ReportNotificationViewHolder(
private val binding: ItemReportNotificationBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun setupWithReport(reporter: TimelineAccount, report: Report, animateAvatar: Boolean, animateEmojis: Boolean) {
val reporterName = reporter.name.unicodeWrap().emojify(reporter.emojis, itemView, animateEmojis)
val reporteeName = report.targetAccount.name.unicodeWrap().emojify(report.targetAccount.emojis, itemView, animateEmojis)
val icon = ContextCompat.getDrawable(itemView.context, R.drawable.ic_flag_24dp)
binding.notificationTopText.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null)
binding.notificationTopText.text = itemView.context.getString(R.string.notification_header_report_format, reporterName, reporteeName)
binding.notificationSummary.text = itemView.context.getString(R.string.notification_summary_report_format, TimestampUtils.getRelativeTimeSpanString(itemView.context, report.createdAt.time, Date().time), report.status_ids?.size ?: 0)
binding.notificationCategory.text = getTranslatedCategory(itemView.context, report.category)
// Fancy avatar inset
val padding = Utils.dpToPx(binding.notificationReporteeAvatar.context, 12)
binding.notificationReporteeAvatar.setPaddingRelative(0, 0, padding, padding)
loadAvatar(
report.targetAccount.avatar,
binding.notificationReporteeAvatar,
itemView.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp),
animateAvatar,
)
loadAvatar(
reporter.avatar,
binding.notificationReporterAvatar,
itemView.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_24dp),
animateAvatar,
)
}
fun setupActionListener(listener: NotificationActionListener, reporteeId: String, reporterId: String, reportId: String) {
binding.notificationReporteeAvatar.setOnClickListener {
val position = bindingAdapterPosition
if (position != RecyclerView.NO_POSITION) {
listener.onViewAccount(reporteeId)
}
}
binding.notificationReporterAvatar.setOnClickListener {
val position = bindingAdapterPosition
if (position != RecyclerView.NO_POSITION) {
listener.onViewAccount(reporterId)
}
}
itemView.setOnClickListener { listener.onViewReport(reportId) }
}
private fun getTranslatedCategory(context: Context, rawCategory: String): String {
return when (rawCategory) {
"violation" -> context.getString(R.string.report_category_violation)
"spam" -> context.getString(R.string.report_category_spam)
"other" -> context.getString(R.string.report_category_other)
else -> rawCategory
}
}
}

View File

@ -1,5 +1,7 @@
package com.keylesspalace.tusky.adapter;
import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription;
import android.content.Context;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
@ -23,6 +25,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.ContextCompat;
import androidx.core.text.HtmlCompat;
import androidx.core.view.ViewKt;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@ -54,6 +57,7 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.util.TimestampUtils;
import com.keylesspalace.tusky.view.MediaPreviewImageView;
import com.keylesspalace.tusky.view.MediaPreviewLayout;
import com.keylesspalace.tusky.viewdata.PollOptionViewData;
import com.keylesspalace.tusky.viewdata.PollViewData;
import com.keylesspalace.tusky.viewdata.PollViewDataKt;
@ -67,14 +71,13 @@ import at.connyduck.sparkbutton.SparkButton;
import at.connyduck.sparkbutton.helpers.Utils;
import kotlin.collections.CollectionsKt;
import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription;
import net.accelf.yuito.QuoteInlineHelper;
public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
public static class Key {
public static final String KEY_CREATED = "created";
}
private TextView displayName;
private TextView username;
private ImageButton replyButton;
@ -85,8 +88,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private SparkButton bookmarkButton;
private ImageButton moreButton;
private ConstraintLayout mediaContainer;
protected MediaPreviewImageView[] mediaPreviews;
private ImageView[] mediaOverlays;
protected MediaPreviewLayout mediaPreview;
private TextView sensitiveMediaWarning;
private View sensitiveMediaShow;
protected TextView[] mediaLabels;
@ -139,19 +141,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
mediaContainer = itemView.findViewById(R.id.status_media_preview_container);
mediaContainer.setClipToOutline(true);
mediaPreview = itemView.findViewById(R.id.status_media_preview);
mediaPreviews = new MediaPreviewImageView[]{
itemView.findViewById(R.id.status_media_preview_0),
itemView.findViewById(R.id.status_media_preview_1),
itemView.findViewById(R.id.status_media_preview_2),
itemView.findViewById(R.id.status_media_preview_3)
};
mediaOverlays = new ImageView[]{
itemView.findViewById(R.id.status_media_overlay_0),
itemView.findViewById(R.id.status_media_overlay_1),
itemView.findViewById(R.id.status_media_overlay_2),
itemView.findViewById(R.id.status_media_overlay_3)
};
sensitiveMediaWarning = itemView.findViewById(R.id.status_sensitive_media_warning);
sensitiveMediaShow = itemView.findViewById(R.id.status_sensitive_media_button);
mediaLabels = new TextView[]{
@ -190,8 +181,6 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
mediaPreviewUnloaded = new ColorDrawable(ThemeUtils.getColor(itemView.getContext(), R.attr.colorBackgroundAccent));
}
protected abstract int getMediaPreviewHeight(Context context);
protected void setDisplayName(String name, List<Emoji> customEmojis, StatusDisplayOptions statusDisplayOptions) {
CharSequence emojifiedName = CustomEmojiHelper.emojify(
name, customEmojis, displayName, statusDisplayOptions.animateEmojis()
@ -327,19 +316,25 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
protected void setCreatedAt(Date createdAt, StatusDisplayOptions statusDisplayOptions) {
protected void setCreatedAt(Date createdAt, Date editedAt, StatusDisplayOptions statusDisplayOptions) {
String timestampText;
if (statusDisplayOptions.useAbsoluteTime()) {
timestampInfo.setText(absoluteTimeFormatter.format(createdAt, true));
timestampText = absoluteTimeFormatter.format(createdAt, true);
} else {
if (createdAt == null) {
timestampInfo.setText("?m");
timestampText = "?m";
} else {
long then = createdAt.getTime();
long now = System.currentTimeMillis();
String readout = TimestampUtils.getRelativeTimeSpanString(timestampInfo.getContext(), then, now);
timestampInfo.setText(readout);
timestampText = readout;
}
}
if (editedAt != null) {
timestampText = timestampInfo.getContext().getString(R.string.post_timestamp_with_edited_indicator, timestampText);
}
timestampInfo.setText(timestampText);
}
private CharSequence getCreatedAtDescription(Date createdAt,
@ -501,64 +496,51 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
Drawable placeholder = blurhash != null ? decodeBlurHash(blurhash) : mediaPreviewUnloaded;
if (TextUtils.isEmpty(previewUrl)) {
imageView.removeFocalPoint();
Glide.with(imageView)
.load(placeholder)
.centerInside()
.into(imageView);
} else {
Focus focus = meta != null ? meta.getFocus() : null;
if (focus != null) { // If there is a focal point for this attachment:
imageView.setFocalPoint(focus);
Glide.with(imageView)
.load(previewUrl)
.placeholder(placeholder)
.centerInside()
.addListener(imageView)
.into(imageView);
} else {
ViewKt.doOnLayout(imageView, view -> {
if (TextUtils.isEmpty(previewUrl)) {
imageView.removeFocalPoint();
Glide.with(imageView)
.load(previewUrl)
.placeholder(placeholder)
.load(placeholder)
.centerInside()
.into(imageView);
} else {
Focus focus = meta != null ? meta.getFocus() : null;
if (focus != null) { // If there is a focal point for this attachment:
imageView.setFocalPoint(focus);
Glide.with(imageView)
.load(previewUrl)
.placeholder(placeholder)
.centerInside()
.addListener(imageView)
.into(imageView);
} else {
imageView.removeFocalPoint();
Glide.with(imageView)
.load(previewUrl)
.placeholder(placeholder)
.centerInside()
.into(imageView);
}
}
}
return null;
});
}
protected void setMediaPreviews(final List<Attachment> attachments, boolean sensitive,
final StatusActionListener listener, boolean showingContent,
boolean useBlurhash) {
Context context = itemView.getContext();
final int n = Math.min(attachments.size(), Status.MAX_MEDIA_ATTACHMENTS);
mediaPreview.setAspectRatios(AttachmentHelper.aspectRatios(attachments));
final int mediaPreviewHeight = getMediaPreviewHeight(context);
if (n <= 2) {
mediaPreviews[0].getLayoutParams().height = mediaPreviewHeight * 2;
mediaPreviews[1].getLayoutParams().height = mediaPreviewHeight * 2;
} else {
mediaPreviews[0].getLayoutParams().height = mediaPreviewHeight;
mediaPreviews[1].getLayoutParams().height = mediaPreviewHeight;
mediaPreviews[2].getLayoutParams().height = mediaPreviewHeight;
mediaPreviews[3].getLayoutParams().height = mediaPreviewHeight;
}
for (int i = 0; i < n; i++) {
mediaPreview.forEachIndexed((i, imageView) -> {
Attachment attachment = attachments.get(i);
String previewUrl = attachment.getPreviewUrl();
String description = attachment.getDescription();
MediaPreviewImageView imageView = mediaPreviews[i];
imageView.setVisibility(View.VISIBLE);
if (TextUtils.isEmpty(description)) {
imageView.setContentDescription(imageView.getContext()
@ -576,42 +558,38 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
final Attachment.Type type = attachment.getType();
if (showingContent && (type == Attachment.Type.VIDEO || type == Attachment.Type.GIFV)) {
mediaOverlays[i].setVisibility(View.VISIBLE);
imageView.setForeground(ContextCompat.getDrawable(itemView.getContext(), R.drawable.play_indicator_overlay));
} else {
mediaOverlays[i].setVisibility(View.GONE);
imageView.setForeground(null);
}
setAttachmentClickListener(imageView, listener, i, attachment, true);
}
if (sensitive) {
sensitiveMediaWarning.setText(R.string.post_sensitive_media_title);
} else {
sensitiveMediaWarning.setText(R.string.post_media_hidden_title);
}
sensitiveMediaWarning.setVisibility(showingContent ? View.GONE : View.VISIBLE);
sensitiveMediaShow.setVisibility(showingContent ? View.VISIBLE : View.GONE);
sensitiveMediaShow.setOnClickListener(v -> {
if (getBindingAdapterPosition() != RecyclerView.NO_POSITION) {
listener.onContentHiddenChange(false, getBindingAdapterPosition());
if (sensitive) {
sensitiveMediaWarning.setText(R.string.post_sensitive_media_title);
} else {
sensitiveMediaWarning.setText(R.string.post_media_hidden_title);
}
v.setVisibility(View.GONE);
sensitiveMediaWarning.setVisibility(View.VISIBLE);
});
sensitiveMediaWarning.setOnClickListener(v -> {
if (getBindingAdapterPosition() != RecyclerView.NO_POSITION) {
listener.onContentHiddenChange(true, getBindingAdapterPosition());
}
v.setVisibility(View.GONE);
sensitiveMediaShow.setVisibility(View.VISIBLE);
});
sensitiveMediaWarning.setVisibility(showingContent ? View.GONE : View.VISIBLE);
sensitiveMediaShow.setVisibility(showingContent ? View.VISIBLE : View.GONE);
sensitiveMediaShow.setOnClickListener(v -> {
if (getBindingAdapterPosition() != RecyclerView.NO_POSITION) {
listener.onContentHiddenChange(false, getBindingAdapterPosition());
}
v.setVisibility(View.GONE);
sensitiveMediaWarning.setVisibility(View.VISIBLE);
});
sensitiveMediaWarning.setOnClickListener(v -> {
if (getBindingAdapterPosition() != RecyclerView.NO_POSITION) {
listener.onContentHiddenChange(true, getBindingAdapterPosition());
}
v.setVisibility(View.GONE);
sensitiveMediaShow.setVisibility(View.VISIBLE);
});
// Hide any of the placeholder previews beyond the ones set.
for (int i = n; i < Status.MAX_MEDIA_ATTACHMENTS; i++) {
mediaPreviews[i].setVisibility(View.GONE);
}
return null;
});
}
@DrawableRes
@ -835,7 +813,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
Status actionable = status.getActionable();
setDisplayName(actionable.getAccount().getName(), actionable.getAccount().getEmojis(), statusDisplayOptions);
setUsername(status.getUsername());
setCreatedAt(actionable.getCreatedAt(), statusDisplayOptions);
setCreatedAt(actionable.getCreatedAt(), actionable.getEditedAt(), statusDisplayOptions);
setStatusVisibility(actionable.getVisibility());
setIsReply(actionable.getInReplyToId() != null);
setReplyCount(actionable.getRepliesCount());
@ -860,10 +838,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
} else {
setMediaLabel(attachments, sensitive, listener, status.isShowingContent());
// Hide all unused views.
mediaPreviews[0].setVisibility(View.GONE);
mediaPreviews[1].setVisibility(View.GONE);
mediaPreviews[2].setVisibility(View.GONE);
mediaPreviews[3].setVisibility(View.GONE);
mediaPreview.setVisibility(View.GONE);
hideSensitiveMediaWarning();
}
@ -893,7 +868,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
if (payloads instanceof List)
for (Object item : (List<?>) payloads) {
if (Key.KEY_CREATED.equals(item)) {
setCreatedAt(status.getActionable().getCreatedAt(), statusDisplayOptions);
setCreatedAt(status.getActionable().getCreatedAt(), status.getActionable().getEditedAt(), statusDisplayOptions);
}
}
@ -919,6 +894,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
getContentWarningDescription(context, status),
(TextUtils.isEmpty(status.getSpoilerText()) || !actionable.getSensitive() || status.isExpanded() ? status.getContent() : ""),
getCreatedAtDescription(actionable.getCreatedAt(), statusDisplayOptions),
actionable.getEditedAt() != null ? context.getString(R.string.description_post_edited) : "",
getReblogDescription(context, status),
status.getUsername(),
actionable.getReblogged() ? context.getString(R.string.description_post_reblogged) : "",

View File

@ -1,6 +1,7 @@
package com.keylesspalace.tusky.adapter;
import android.content.Context;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.view.View;
import android.widget.TextView;
@ -18,7 +19,9 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.viewdata.StatusViewData;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class StatusDetailedViewHolder extends StatusBaseViewHolder {
private final TextView reblogs;
@ -33,18 +36,17 @@ public class StatusDetailedViewHolder extends StatusBaseViewHolder {
}
@Override
protected int getMediaPreviewHeight(Context context) {
return context.getResources().getDimensionPixelSize(R.dimen.status_detail_media_preview_height);
}
@Override
protected void setCreatedAt(Date createdAt, StatusDisplayOptions statusDisplayOptions) {
if (createdAt == null) {
timestampInfo.setText("");
} else {
DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.SHORT);
timestampInfo.setText(dateFormat.format(createdAt));
protected void setCreatedAt(Date createdAt, Date editedAt, StatusDisplayOptions statusDisplayOptions) {
DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.SHORT);
Context context = timestampInfo.getContext();
List<String> list = new ArrayList<>();
if (createdAt != null) {
list.add(dateFormat.format(createdAt));
}
if (editedAt != null) {
list.add(context.getString(R.string.post_edited, dateFormat.format(editedAt)));
}
timestampInfo.setText(TextUtils.join(context.getString(R.string.timestamp_joiner), list));
}
private void setReblogAndFavCount(int reblogCount, int favCount, StatusActionListener listener) {

View File

@ -53,11 +53,6 @@ public class StatusViewHolder extends StatusBaseViewHolder {
contentCollapseButton = itemView.findViewById(R.id.button_toggle_content);
}
@Override
protected int getMediaPreviewHeight(Context context) {
return context.getResources().getDimensionPixelSize(R.dimen.status_media_preview_height);
}
@Override
public void setupWithStatus(@NonNull StatusViewData.Concrete status,
@NonNull final StatusActionListener listener,

View File

@ -53,6 +53,7 @@ import com.keylesspalace.tusky.EditProfileActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.StatusListActivity
import com.keylesspalace.tusky.ViewMediaActivity
import com.keylesspalace.tusky.components.account.list.ListsForAccountFragment
import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.report.ReportActivity
import com.keylesspalace.tusky.databinding.ActivityAccountBinding
@ -103,6 +104,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
private lateinit var accountFieldAdapter: AccountFieldAdapter
private val preferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
private var followState: FollowState = FollowState.NOT_FOLLOWING
private var blocking: Boolean = false
private var muting: Boolean = false
@ -247,6 +250,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
val pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin)
binding.accountFragmentViewPager.setPageTransformer(MarginPageTransformer(pageMargin))
val enableSwipeForTabs = preferences.getBoolean(PrefKeys.ENABLE_SWIPE_FOR_TABS, true)
binding.accountFragmentViewPager.isUserInputEnabled = enableSwipeForTabs
binding.accountTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabReselected(tab: TabLayout.Tab?) {
tab?.position?.let { position ->
@ -737,6 +743,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
menu.removeItem(R.id.action_report)
}
if (!viewModel.isSelf && followState != FollowState.FOLLOWING) {
menu.removeItem(R.id.action_add_or_remove_from_list)
}
return super.onCreateOptionsMenu(menu)
}
@ -849,6 +859,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
toggleMute()
return true
}
R.id.action_add_or_remove_from_list -> {
ListsForAccountFragment.newInstance(viewModel.accountId).show(supportFragmentManager, null)
return true
}
R.id.action_mute_domain -> {
toggleBlockDomain(domain)
return true

View File

@ -0,0 +1,210 @@
/* Copyright 2022 kyori19
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>.
*/
package com.keylesspalace.tusky.components.account.list
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.FragmentListsForAccountBinding
import com.keylesspalace.tusky.databinding.ItemAddOrRemoveFromListBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.util.BindingHolder
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.io.IOException
import javax.inject.Inject
class ListsForAccountFragment : DialogFragment(), Injectable {
@Inject
lateinit var viewModelFactory: ViewModelFactory
private val viewModel: ListsForAccountViewModel by viewModels { viewModelFactory }
private val binding by viewBinding(FragmentListsForAccountBinding::bind)
private val adapter = Adapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, R.style.TuskyDialogFragmentStyle)
viewModel.setup(requireArguments().getString(ARG_ACCOUNT_ID)!!)
}
override fun onStart() {
super.onStart()
dialog?.apply {
window?.setLayout(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT,
)
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_lists_for_account, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.listsView.layoutManager = LinearLayoutManager(view.context)
binding.listsView.adapter = adapter
viewLifecycleOwner.lifecycleScope.launch {
viewModel.states.collectLatest { states ->
binding.progressBar.hide()
if (states.isEmpty()) {
binding.messageView.show()
binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.no_lists) {
load()
}
} else {
binding.listsView.show()
adapter.submitList(states)
}
}
}
viewLifecycleOwner.lifecycleScope.launch {
viewModel.loadError.collectLatest { error ->
binding.progressBar.hide()
binding.listsView.hide()
binding.messageView.apply {
show()
if (error is IOException) {
setup(R.drawable.elephant_offline, R.string.error_network) {
load()
}
} else {
setup(R.drawable.elephant_error, R.string.error_generic) {
load()
}
}
}
}
}
viewLifecycleOwner.lifecycleScope.launch {
viewModel.actionError.collectLatest { error ->
when (error.type) {
ActionError.Type.ADD -> {
Snackbar.make(binding.root, R.string.failed_to_add_to_list, Snackbar.LENGTH_LONG)
.setAction(R.string.action_retry) {
viewModel.addAccountToList(error.listId)
}
.show()
}
ActionError.Type.REMOVE -> {
Snackbar.make(binding.root, R.string.failed_to_remove_from_list, Snackbar.LENGTH_LONG)
.setAction(R.string.action_retry) {
viewModel.removeAccountFromList(error.listId)
}
.show()
}
}
}
}
binding.doneButton.setOnClickListener {
dismiss()
}
load()
}
private fun load() {
binding.progressBar.show()
binding.listsView.hide()
binding.messageView.hide()
viewModel.load()
}
private object Differ : DiffUtil.ItemCallback<AccountListState>() {
override fun areItemsTheSame(
oldItem: AccountListState,
newItem: AccountListState
): Boolean {
return oldItem.list.id == newItem.list.id
}
override fun areContentsTheSame(
oldItem: AccountListState,
newItem: AccountListState
): Boolean {
return oldItem == newItem
}
}
inner class Adapter :
ListAdapter<AccountListState, BindingHolder<ItemAddOrRemoveFromListBinding>>(Differ) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): BindingHolder<ItemAddOrRemoveFromListBinding> {
val binding =
ItemAddOrRemoveFromListBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return BindingHolder(binding)
}
override fun onBindViewHolder(holder: BindingHolder<ItemAddOrRemoveFromListBinding>, position: Int) {
val item = getItem(position)
holder.binding.listNameView.text = item.list.title
holder.binding.addButton.apply {
visible(!item.includesAccount)
setOnClickListener {
viewModel.addAccountToList(item.list.id)
}
}
holder.binding.removeButton.apply {
visible(item.includesAccount)
setOnClickListener {
viewModel.removeAccountFromList(item.list.id)
}
}
}
}
companion object {
private const val ARG_ACCOUNT_ID = "accountId"
fun newInstance(accountId: String): ListsForAccountFragment {
val args = Bundle().apply {
putString(ARG_ACCOUNT_ID, accountId)
}
return ListsForAccountFragment().apply { arguments = args }
}
}
}

View File

@ -0,0 +1,137 @@
/* Copyright 2022 kyori19
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>.
*/
package com.keylesspalace.tusky.components.account.list
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import at.connyduck.calladapter.networkresult.getOrThrow
import at.connyduck.calladapter.networkresult.onFailure
import at.connyduck.calladapter.networkresult.onSuccess
import at.connyduck.calladapter.networkresult.runCatching
import com.keylesspalace.tusky.entity.MastoList
import com.keylesspalace.tusky.network.MastodonApi
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import javax.inject.Inject
data class AccountListState(
val list: MastoList,
val includesAccount: Boolean,
)
data class ActionError(
val error: Throwable,
val type: Type,
val listId: String,
) : Throwable(error) {
enum class Type {
ADD,
REMOVE,
}
}
@OptIn(ExperimentalCoroutinesApi::class)
class ListsForAccountViewModel @Inject constructor(
private val mastodonApi: MastodonApi,
) : ViewModel() {
private lateinit var accountId: String
private val _states = MutableSharedFlow<List<AccountListState>>(1)
val states: SharedFlow<List<AccountListState>> = _states
private val _loadError = MutableSharedFlow<Throwable>(1)
val loadError: SharedFlow<Throwable> = _loadError
private val _actionError = MutableSharedFlow<ActionError>(1)
val actionError: SharedFlow<ActionError> = _actionError
fun setup(accountId: String) {
this.accountId = accountId
}
fun load() {
_loadError.resetReplayCache()
viewModelScope.launch {
runCatching {
val (all, includes) = listOf(
async { mastodonApi.getLists() },
async { mastodonApi.getListsIncludesAccount(accountId) },
).awaitAll()
_states.emit(
all.getOrThrow().map { list ->
AccountListState(
list = list,
includesAccount = includes.getOrThrow().any { it.id == list.id },
)
}
)
}
.onFailure {
_loadError.emit(it)
}
}
}
fun addAccountToList(listId: String) {
_actionError.resetReplayCache()
viewModelScope.launch {
mastodonApi.addAccountToList(listId, listOf(accountId))
.onSuccess {
_states.emit(
_states.first().map { state ->
if (state.list.id == listId) {
state.copy(includesAccount = true)
} else {
state
}
}
)
}
.onFailure {
_actionError.emit(ActionError(it, ActionError.Type.ADD, listId))
}
}
}
fun removeAccountFromList(listId: String) {
_actionError.resetReplayCache()
viewModelScope.launch {
mastodonApi.deleteAccountFromList(listId, listOf(accountId))
.onSuccess {
_states.emit(
_states.first().map { state ->
if (state.list.id == listId) {
state.copy(includesAccount = false)
} else {
state
}
}
)
}
.onFailure {
_actionError.emit(ActionError(it, ActionError.Type.REMOVE, listId))
}
}
}
}

View File

@ -16,7 +16,6 @@
package com.keylesspalace.tusky.components.account.media
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.core.app.ActivityOptionsCompat
import androidx.core.view.ViewCompat
@ -39,7 +38,6 @@ import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.openLink
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.viewdata.AttachmentViewData
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@ -107,21 +105,30 @@ class AccountMediaFragment :
}
adapter.addLoadStateListener { loadState ->
binding.progressBar.visible(loadState.refresh == LoadState.Loading && adapter.itemCount == 0)
binding.statusView.hide()
binding.progressBar.hide()
if (loadState.refresh is LoadState.Error) {
binding.recyclerView.hide()
binding.statusView.show()
val errorState = loadState.refresh as LoadState.Error
if (errorState.error is IOException) {
binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network) { adapter.retry() }
} else {
binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic) { adapter.retry() }
if (adapter.itemCount == 0) {
when (loadState.refresh) {
is LoadState.NotLoading -> {
if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) {
binding.statusView.show()
binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null)
}
}
is LoadState.Error -> {
binding.statusView.show()
if ((loadState.refresh as LoadState.Error).error is IOException) {
binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network, null)
} else {
binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic, null)
}
}
is LoadState.Loading -> {
binding.progressBar.show()
}
}
Log.w(TAG, "error loading account media", errorState.error)
} else {
binding.recyclerView.show()
binding.statusView.hide()
}
}
}

View File

@ -92,10 +92,13 @@ import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.PickMediaFiles
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.afterTextChanged
import com.keylesspalace.tusky.util.getInitialLanguage
import com.keylesspalace.tusky.util.getLocaleList
import com.keylesspalace.tusky.util.getMediaSize
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.highlightSpans
import com.keylesspalace.tusky.util.loadAvatar
import com.keylesspalace.tusky.util.modernLanguageCode
import com.keylesspalace.tusky.util.onTextChanged
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
@ -269,7 +272,7 @@ class ComposeActivity :
binding.composeScheduleView.setDateTime(composeOptions?.scheduledAt)
}
setupLanguageSpinner(getInitialLanguage(composeOptions?.language))
setupLanguageSpinner(getInitialLanguage(composeOptions?.language, accountManager.activeAccount))
setupComposeField(preferences, viewModel.startingText)
setupDefaultTagViews(preferences)
setupContentWarningField(composeOptions?.contentWarning)
@ -604,37 +607,19 @@ class ComposeActivity :
)
}
private fun setupLanguageSpinner(initialLanguage: String?) {
val locales = Locale.getAvailableLocales()
.filter { it.country.isNullOrEmpty() && it.script.isNullOrEmpty() && it.variant.isNullOrEmpty() } // Only "base" languages, "en" but not "en_DK"
var currentLocaleIndex = locales.indexOfFirst { it.language == initialLanguage }
if (currentLocaleIndex < 0) {
Log.e(TAG, "Error looking up language tag '$initialLanguage', falling back to english")
currentLocaleIndex = locales.indexOfFirst { it.language == "en" }
}
val context = this
private fun setupLanguageSpinner(initialLanguage: String) {
binding.composePostLanguageButton.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
viewModel.postLanguage = (parent.adapter.getItem(position) as Locale).language
viewModel.postLanguage = (parent.adapter.getItem(position) as Locale).modernLanguageCode
}
override fun onNothingSelected(parent: AdapterView<*>) {
parent.setSelection(locales.indexOfFirst { it.language == getInitialLanguage() })
parent.setSelection(0)
}
}
binding.composePostLanguageButton.apply {
adapter = LocaleAdapter(context, android.R.layout.simple_spinner_dropdown_item, locales)
setSelection(currentLocaleIndex)
}
}
private fun getInitialLanguage(language: String? = null): String {
return if (language.isNullOrEmpty()) {
// Setting the application ui preference sets the default locale
Locale.getDefault().language
} else {
language
adapter = LocaleAdapter(context, android.R.layout.simple_spinner_dropdown_item, getLocaleList(initialLanguage))
setSelection(0)
}
}
@ -874,7 +859,7 @@ class ComposeActivity :
// Wait until bottom sheet is not collapsed and show next screen after
if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
addMediaBehavior.removeBottomSheetCallback(this)
if (ContextCompat.checkSelfPermission(this@ComposeActivity, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(this@ComposeActivity, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(
this@ComposeActivity,
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),

View File

@ -80,6 +80,7 @@ data class ConversationStatusEntity(
val account: ConversationAccountEntity,
val content: String,
val createdAt: Date,
val editedAt: Date?,
val emojis: List<Emoji>,
val favouritesCount: Int,
val repliesCount: Int,
@ -109,6 +110,7 @@ data class ConversationStatusEntity(
content = content,
reblog = null,
createdAt = createdAt,
editedAt = editedAt,
emojis = emojis,
reblogsCount = 0,
favouritesCount = favouritesCount,
@ -147,7 +149,11 @@ fun TimelineAccount.toEntity() =
emojis = emojis ?: emptyList()
)
fun Status.toEntity() =
fun Status.toEntity(
expanded: Boolean,
contentShowing: Boolean,
contentCollapsed: Boolean
) =
ConversationStatusEntity(
id = id,
url = url,
@ -156,6 +162,7 @@ fun Status.toEntity() =
account = account.toEntity(),
content = content,
createdAt = createdAt,
editedAt = editedAt,
emojis = emojis,
favouritesCount = favouritesCount,
repliesCount = repliesCount,
@ -166,20 +173,30 @@ fun Status.toEntity() =
attachments = attachments,
mentions = mentions,
tags = tags,
showingHiddenContent = false,
expanded = false,
collapsed = true,
showingHiddenContent = contentShowing,
expanded = expanded,
collapsed = contentCollapsed,
muted = muted ?: false,
poll = poll,
language = language,
)
fun Conversation.toEntity(accountId: Long, order: Int) =
fun Conversation.toEntity(
accountId: Long,
order: Int,
expanded: Boolean,
contentShowing: Boolean,
contentCollapsed: Boolean
) =
ConversationEntity(
accountId = accountId,
id = id,
order = order,
accounts = accounts.map { it.toEntity() },
unread = unread,
lastStatus = lastStatus!!.toEntity()
lastStatus = lastStatus!!.toEntity(
expanded = expanded,
contentShowing = contentShowing,
contentCollapsed = contentCollapsed
)
)

View File

@ -71,6 +71,7 @@ fun StatusViewData.Concrete.toConversationStatusEntity(
account = status.account.toEntity(),
content = status.content,
createdAt = status.createdAt,
editedAt = status.editedAt,
emojis = status.emojis,
favouritesCount = status.favouritesCount,
repliesCount = status.repliesCount,

View File

@ -68,11 +68,6 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
this.listener = listener;
}
@Override
protected int getMediaPreviewHeight(Context context) {
return context.getResources().getDimensionPixelSize(R.dimen.status_media_preview_height);
}
void setupWithConversation(
@NonNull ConversationViewData conversation,
@Nullable Object payloads
@ -88,7 +83,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
setDisplayName(account.getDisplayName(), account.getEmojis(), statusDisplayOptions);
setUsername(account.getUsername());
setCreatedAt(status.getCreatedAt(), statusDisplayOptions);
setCreatedAt(status.getCreatedAt(), status.getEditedAt(), statusDisplayOptions);
setIsReply(status.getInReplyToId() != null);
setFavourited(status.getFavourited());
setBookmarked(status.getBookmarked());
@ -108,10 +103,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
} else {
setMediaLabel(attachments, sensitive, listener, statusViewData.isShowingContent());
// Hide all unused views.
mediaPreviews[0].setVisibility(View.GONE);
mediaPreviews[1].setVisibility(View.GONE);
mediaPreviews[2].setVisibility(View.GONE);
mediaPreviews[3].setVisibility(View.GONE);
mediaPreview.setVisibility(View.GONE);
hideSensitiveMediaWarning();
}
@ -129,7 +121,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
if (payloads instanceof List) {
for (Object item : (List<?>) payloads) {
if (Key.KEY_CREATED.equals(item)) {
setCreatedAt(status.getCreatedAt(), statusDisplayOptions);
setCreatedAt(status.getCreatedAt(), status.getEditedAt(), statusDisplayOptions);
}
}
}

View File

@ -5,6 +5,7 @@ import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.room.withTransaction
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.HttpHeaderLink
@ -12,15 +13,17 @@ import retrofit2.HttpException
@OptIn(ExperimentalPagingApi::class)
class ConversationsRemoteMediator(
private val accountId: Long,
private val api: MastodonApi,
private val db: AppDatabase
private val db: AppDatabase,
accountManager: AccountManager,
) : RemoteMediator<Int, ConversationEntity>() {
private var nextKey: String? = null
private var order: Int = 0
private val activeAccount = accountManager.activeAccount!!
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, ConversationEntity>
@ -46,7 +49,7 @@ class ConversationsRemoteMediator(
db.withTransaction {
if (loadType == LoadType.REFRESH) {
db.conversationDao().deleteForAccount(accountId)
db.conversationDao().deleteForAccount(activeAccount.id)
}
val linkHeader = conversationsResponse.headers()["Link"]
@ -56,8 +59,19 @@ class ConversationsRemoteMediator(
db.conversationDao().insert(
conversations
.filterNot { it.lastStatus == null }
.map {
it.toEntity(accountId, order++)
.map { conversation ->
val expanded = activeAccount.alwaysOpenSpoiler
val contentShowing = activeAccount.alwaysShowSensitiveMedia || !conversation.lastStatus!!.sensitive
val contentCollapsed = true
conversation.toEntity(
accountId = activeAccount.id,
order = order++,
expanded = expanded,
contentShowing = contentShowing,
contentCollapsed = contentCollapsed
)
}
)
}

View File

@ -27,6 +27,7 @@ import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.usecase.TimelineCases
import com.keylesspalace.tusky.util.EmptyPagingSource
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.rx3.await
@ -42,8 +43,15 @@ class ConversationsViewModel @Inject constructor(
@OptIn(ExperimentalPagingApi::class)
val conversationFlow = Pager(
config = PagingConfig(pageSize = 30),
remoteMediator = ConversationsRemoteMediator(accountManager.activeAccount!!.id, api, database),
pagingSourceFactory = { database.conversationDao().conversationsForAccount(accountManager.activeAccount!!.id) }
remoteMediator = ConversationsRemoteMediator(api, database, accountManager),
pagingSourceFactory = {
val activeAccount = accountManager.activeAccount
if (activeAccount == null) {
EmptyPagingSource()
} else {
database.conversationDao().conversationsForAccount(activeAccount.id)
}
}
)
.flow
.map { pagingData ->

View File

@ -0,0 +1,148 @@
package com.keylesspalace.tusky.components.followedtags
import android.os.Bundle
import android.util.Log
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.paging.LoadState
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator
import at.connyduck.calladapter.networkresult.fold
import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ActivityFollowedTagsBinding
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.interfaces.HashtagActionListener
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.io.IOException
import javax.inject.Inject
class FollowedTagsActivity : BaseActivity(), HashtagActionListener {
@Inject
lateinit var api: MastodonApi
@Inject
lateinit var viewModelFactory: ViewModelFactory
private val binding by viewBinding(ActivityFollowedTagsBinding::inflate)
private val viewModel: FollowedTagsViewModel by viewModels { viewModelFactory }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
setSupportActionBar(binding.includedToolbar.toolbar)
supportActionBar?.run {
setTitle(R.string.title_followed_hashtags)
// Back button
setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true)
}
setupAdapter().let { adapter ->
setupRecyclerView(adapter)
lifecycleScope.launch {
viewModel.pager.collectLatest { pagingData ->
adapter.submitData(pagingData)
}
}
}
}
private fun setupRecyclerView(adapter: FollowedTagsAdapter) {
binding.followedTagsView.adapter = adapter
binding.followedTagsView.setHasFixedSize(true)
binding.followedTagsView.layoutManager = LinearLayoutManager(this)
binding.followedTagsView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
(binding.followedTagsView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
}
private fun setupAdapter(): FollowedTagsAdapter {
return FollowedTagsAdapter(this, viewModel).apply {
addLoadStateListener { loadState ->
binding.followedTagsProgressBar.visible(loadState.refresh == LoadState.Loading && itemCount == 0)
if (loadState.refresh is LoadState.Error) {
binding.followedTagsView.hide()
binding.followedTagsMessageView.show()
val errorState = loadState.refresh as LoadState.Error
if (errorState.error is IOException) {
binding.followedTagsMessageView.setup(R.drawable.elephant_offline, R.string.error_network) { retry() }
} else {
binding.followedTagsMessageView.setup(R.drawable.elephant_error, R.string.error_generic) { retry() }
}
Log.w(TAG, "error loading followed hashtags", errorState.error)
} else {
binding.followedTagsView.show()
binding.followedTagsMessageView.hide()
}
}
}
}
private fun follow(tagName: String, position: Int) {
lifecycleScope.launch {
api.followTag(tagName).fold(
{
viewModel.tags.add(position, it)
viewModel.currentSource?.invalidate()
},
{
Snackbar.make(
this@FollowedTagsActivity,
binding.followedTagsView,
getString(R.string.error_following_hashtag_format, tagName),
Snackbar.LENGTH_SHORT
)
.show()
}
)
}
}
override fun unfollow(tagName: String, position: Int) {
lifecycleScope.launch {
api.unfollowTag(tagName).fold(
{
viewModel.tags.removeAt(position)
Snackbar.make(
this@FollowedTagsActivity,
binding.followedTagsView,
getString(R.string.confirmation_hashtag_unfollowed, tagName),
Snackbar.LENGTH_LONG
)
.setAction(R.string.action_undo) {
follow(tagName, position)
}
.show()
viewModel.currentSource?.invalidate()
},
{
Snackbar.make(
this@FollowedTagsActivity,
binding.followedTagsView,
getString(
R.string.error_unfollowing_hashtag_format,
tagName
),
Snackbar.LENGTH_SHORT
)
.show()
}
)
}
}
companion object {
const val TAG = "FollowedTagsActivity"
}
}

View File

@ -0,0 +1,38 @@
package com.keylesspalace.tusky.components.followedtags
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.TextView
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemFollowedHashtagBinding
import com.keylesspalace.tusky.interfaces.HashtagActionListener
import com.keylesspalace.tusky.util.BindingHolder
class FollowedTagsAdapter(
private val actionListener: HashtagActionListener,
private val viewModel: FollowedTagsViewModel,
) : PagingDataAdapter<String, BindingHolder<ItemFollowedHashtagBinding>>(STRING_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowedHashtagBinding> =
BindingHolder(ItemFollowedHashtagBinding.inflate(LayoutInflater.from(parent.context), parent, false))
override fun onBindViewHolder(holder: BindingHolder<ItemFollowedHashtagBinding>, position: Int) {
viewModel.tags[position].let { tag ->
holder.itemView.findViewById<TextView>(R.id.followed_tag).text = tag.name
holder.itemView.findViewById<ImageButton>(R.id.followed_tag_unfollow).setOnClickListener {
actionListener.unfollow(tag.name, position)
}
}
}
override fun getItemCount(): Int = viewModel.tags.size
companion object {
val STRING_COMPARATOR = object : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean = oldItem == newItem
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean = oldItem == newItem
}
}
}

View File

@ -0,0 +1,16 @@
package com.keylesspalace.tusky.components.followedtags
import androidx.paging.PagingSource
import androidx.paging.PagingState
class FollowedTagsPagingSource(private val viewModel: FollowedTagsViewModel) : PagingSource<String, String>() {
override fun getRefreshKey(state: PagingState<String, String>): String? = null
override suspend fun load(params: LoadParams<String>): LoadResult<String, String> {
return if (params is LoadParams.Refresh) {
LoadResult.Page(viewModel.tags.map { it.name }, null, viewModel.nextKey)
} else {
LoadResult.Page(emptyList(), null, null)
}
}
}

View File

@ -0,0 +1,57 @@
package com.keylesspalace.tusky.components.followedtags
import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import com.keylesspalace.tusky.entity.HashTag
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.HttpHeaderLink
import retrofit2.HttpException
import retrofit2.Response
@OptIn(ExperimentalPagingApi::class)
class FollowedTagsRemoteMediator(
private val api: MastodonApi,
private val viewModel: FollowedTagsViewModel,
) : RemoteMediator<String, String>() {
override suspend fun load(
loadType: LoadType,
state: PagingState<String, String>
): MediatorResult {
return try {
val response = request(loadType)
?: return MediatorResult.Success(endOfPaginationReached = true)
return applyResponse(response)
} catch (e: Exception) {
MediatorResult.Error(e)
}
}
private suspend fun request(loadType: LoadType): Response<List<HashTag>>? {
return when (loadType) {
LoadType.PREPEND -> null
LoadType.APPEND -> api.followedTags(maxId = viewModel.nextKey)
LoadType.REFRESH -> {
viewModel.nextKey = null
viewModel.tags.clear()
api.followedTags()
}
}
}
private fun applyResponse(response: Response<List<HashTag>>): MediatorResult {
val tags = response.body()
if (!response.isSuccessful || tags == null) {
return MediatorResult.Error(HttpException(response))
}
val links = HttpHeaderLink.parse(response.headers()["Link"])
viewModel.nextKey = HttpHeaderLink.findByRelationType(links, "next")?.uri?.getQueryParameter("max_id")
viewModel.tags.addAll(tags)
viewModel.currentSource?.invalidate()
return MediatorResult.Success(endOfPaginationReached = viewModel.nextKey == null)
}
}

View File

@ -0,0 +1,33 @@
package com.keylesspalace.tusky.components.followedtags
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.ExperimentalPagingApi
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.cachedIn
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.HashTag
import com.keylesspalace.tusky.network.MastodonApi
import javax.inject.Inject
class FollowedTagsViewModel @Inject constructor (
api: MastodonApi
) : ViewModel(), Injectable {
val tags: MutableList<HashTag> = mutableListOf()
var nextKey: String? = null
var currentSource: FollowedTagsPagingSource? = null
@OptIn(ExperimentalPagingApi::class)
val pager = Pager(
config = PagingConfig(pageSize = 100),
remoteMediator = FollowedTagsRemoteMediator(api, this),
pagingSourceFactory = {
FollowedTagsPagingSource(
viewModel = this
).also { source ->
currentSource = source
}
},
).flow.cachedIn(viewModelScope)
}

View File

@ -120,6 +120,7 @@ public class NotificationHelper {
public static final String CHANNEL_SUBSCRIPTIONS = "CHANNEL_SUBSCRIPTIONS";
public static final String CHANNEL_SIGN_UP = "CHANNEL_SIGN_UP";
public static final String CHANNEL_UPDATES = "CHANNEL_UPDATES";
public static final String CHANNEL_REPORT = "CHANNEL_REPORT";
/**
* WorkManager Tag
@ -401,6 +402,7 @@ public class NotificationHelper {
CHANNEL_SUBSCRIPTIONS + account.getIdentifier(),
CHANNEL_SIGN_UP + account.getIdentifier(),
CHANNEL_UPDATES + account.getIdentifier(),
CHANNEL_REPORT + account.getIdentifier(),
};
int[] channelNames = {
R.string.notification_mention_name,
@ -412,6 +414,7 @@ public class NotificationHelper {
R.string.notification_subscription_name,
R.string.notification_sign_up_name,
R.string.notification_update_name,
R.string.notification_report_name,
};
int[] channelDescriptions = {
R.string.notification_mention_descriptions,
@ -423,6 +426,7 @@ public class NotificationHelper {
R.string.notification_subscription_description,
R.string.notification_sign_up_description,
R.string.notification_update_description,
R.string.notification_report_description,
};
List<NotificationChannel> channels = new ArrayList<>(6);
@ -469,7 +473,7 @@ public class NotificationHelper {
if (notificationManager.areNotificationsEnabled()) {
for (NotificationChannel channel : notificationManager.getNotificationChannels()) {
if (channel.getImportance() > NotificationManager.IMPORTANCE_NONE) {
if (channel != null && channel.getImportance() > NotificationManager.IMPORTANCE_NONE) {
Log.d(TAG, "NotificationsEnabled");
return true;
}
@ -542,7 +546,7 @@ public class NotificationHelper {
return false;
}
NotificationChannel channel = notificationManager.getNotificationChannel(channelId);
return channel.getImportance() > NotificationManager.IMPORTANCE_NONE;
return channel != null && channel.getImportance() > NotificationManager.IMPORTANCE_NONE;
}
switch (type) {
@ -564,6 +568,8 @@ public class NotificationHelper {
return account.getNotificationsSignUps();
case UPDATE:
return account.getNotificationsUpdates();
case REPORT:
return account.getNotificationsReports();
default:
return false;
}
@ -593,6 +599,10 @@ public class NotificationHelper {
return CHANNEL_POLL + account.getIdentifier();
case SIGN_UP:
return CHANNEL_SIGN_UP + account.getIdentifier();
case UPDATE:
return CHANNEL_UPDATES + account.getIdentifier();
case REPORT:
return CHANNEL_REPORT + account.getIdentifier();
default:
return null;
}
@ -678,6 +688,8 @@ public class NotificationHelper {
return String.format(context.getString(R.string.notification_sign_up_format), accountName);
case UPDATE:
return String.format(context.getString(R.string.notification_update_format), accountName);
case REPORT:
return context.getString(R.string.notification_report_format, account.getDomain());
}
return null;
}
@ -715,6 +727,12 @@ public class NotificationHelper {
}
return builder.toString();
}
case REPORT:
return context.getString(
R.string.notification_header_report_format,
StringUtils.unicodeWrap(notification.getAccount().getName()),
StringUtils.unicodeWrap(notification.getReport().getTargetAccount().getName())
);
}
return null;
}

View File

@ -30,6 +30,7 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.TabPreferenceActivity
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
import com.keylesspalace.tusky.components.followedtags.FollowedTagsActivity
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
import com.keylesspalace.tusky.components.login.LoginActivity
import com.keylesspalace.tusky.components.notifications.currentAccountNeedsMigration
@ -47,6 +48,10 @@ import com.keylesspalace.tusky.settings.preference
import com.keylesspalace.tusky.settings.preferenceCategory
import com.keylesspalace.tusky.settings.switchPreference
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.getInitialLanguage
import com.keylesspalace.tusky.util.getLocaleList
import com.keylesspalace.tusky.util.getTuskyDisplayName
import com.keylesspalace.tusky.util.makeIcon
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
@ -66,6 +71,8 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
@Inject
lateinit var eventHub: EventHub
private val iconSize by lazy { resources.getDimensionPixelSize(R.dimen.preference_icon_size) }
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
val context = requireContext()
makePreferenceScreen {
@ -95,6 +102,20 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
}
}
preference {
setTitle(R.string.title_followed_hashtags)
setIcon(R.drawable.ic_hashtag)
setOnPreferenceClickListener {
val intent = Intent(context, FollowedTagsActivity::class.java)
activity?.startActivity(intent)
activity?.overridePendingTransition(
R.anim.slide_from_right,
R.anim.slide_to_left
)
true
}
}
preference {
setTitle(R.string.action_view_mutes)
setIcon(R.drawable.ic_mute_24dp)
@ -173,6 +194,29 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
}
}
listPreference {
val locales = getLocaleList(getInitialLanguage(null, accountManager.activeAccount))
setTitle(R.string.pref_default_post_language)
// Explicitly add "System default" to the start of the list
entries = (
listOf(context.getString(R.string.system_default)) + locales.map {
it.getTuskyDisplayName(context)
}
).toTypedArray()
entryValues = (listOf("") + locales.map { it.language }).toTypedArray()
key = PrefKeys.DEFAULT_POST_LANGUAGE
icon = makeIcon(requireContext(), GoogleMaterial.Icon.gmd_translate, iconSize)
value = accountManager.activeAccount?.defaultPostLanguage ?: ""
isPersistent = false // This will be entirely server-driven
setSummaryProvider { entry }
setOnPreferenceChangeListener { _, newValue ->
syncWithServer(language = (newValue as String))
eventHub.dispatch(PreferenceChangedEvent(key))
true
}
}
switchPreference {
setTitle(R.string.pref_default_media_sensitivity)
setIcon(R.drawable.ic_eye_24dp)
@ -301,8 +345,8 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
}
}
private fun syncWithServer(visibility: String? = null, sensitive: Boolean? = null) {
mastodonApi.accountUpdateSource(visibility, sensitive)
private fun syncWithServer(visibility: String? = null, sensitive: Boolean? = null, language: String? = null) {
mastodonApi.accountUpdateSource(visibility, sensitive, language)
.enqueue(object : Callback<Account> {
override fun onResponse(call: Call<Account>, response: Response<Account>) {
val account = response.body()
@ -312,6 +356,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
it.defaultPostPrivacy = account.source?.privacy
?: Status.Visibility.PUBLIC
it.defaultMediaSensitivity = account.source?.sensitive ?: false
it.defaultPostLanguage = language ?: ""
accountManager.saveAccount(it)
}
} else {

View File

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

View File

@ -72,30 +72,17 @@ class PreferencesActivity :
setDisplayShowHomeEnabled(true)
}
val fragmentTag = "preference_fragment_$EXTRA_PREFERENCE_TYPE"
val preferenceType = intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)
val fragmentTag = "preference_fragment_$preferenceType"
val fragment: Fragment = supportFragmentManager.findFragmentByTag(fragmentTag)
?: when (intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)) {
GENERAL_PREFERENCES -> {
setTitle(R.string.action_view_preferences)
PreferencesFragment.newInstance()
}
ACCOUNT_PREFERENCES -> {
setTitle(R.string.action_view_account_preferences)
AccountPreferencesFragment.newInstance()
}
NOTIFICATION_PREFERENCES -> {
setTitle(R.string.pref_title_edit_notification_settings)
NotificationPreferencesFragment.newInstance()
}
TAB_FILTER_PREFERENCES -> {
setTitle(R.string.pref_title_post_tabs)
TabFilterPreferencesFragment.newInstance()
}
PROXY_PREFERENCES -> {
setTitle(R.string.pref_title_http_proxy_settings)
ProxyPreferencesFragment.newInstance()
}
?: when (preferenceType) {
GENERAL_PREFERENCES -> PreferencesFragment.newInstance()
ACCOUNT_PREFERENCES -> AccountPreferencesFragment.newInstance()
NOTIFICATION_PREFERENCES -> NotificationPreferencesFragment.newInstance()
TAB_FILTER_PREFERENCES -> TabFilterPreferencesFragment.newInstance()
PROXY_PREFERENCES -> ProxyPreferencesFragment.newInstance()
else -> throw IllegalArgumentException("preferenceType not known")
}
@ -103,6 +90,14 @@ class PreferencesActivity :
replace(R.id.fragment_container, fragment, fragmentTag)
}
when (preferenceType) {
GENERAL_PREFERENCES -> setTitle(R.string.action_view_preferences)
ACCOUNT_PREFERENCES -> setTitle(R.string.action_view_account_preferences)
NOTIFICATION_PREFERENCES -> setTitle(R.string.pref_title_edit_notification_settings)
TAB_FILTER_PREFERENCES -> setTitle(R.string.pref_title_post_tabs)
PROXY_PREFERENCES -> setTitle(R.string.pref_title_http_proxy_settings)
}
onBackPressedDispatcher.addCallback(this, restartActivitiesOnBackPressedCallback)
restartActivitiesOnBackPressedCallback.isEnabled = savedInstanceState?.getBoolean(EXTRA_RESTART_ON_BACK, false) ?: false
}
@ -141,10 +136,6 @@ class PreferencesActivity :
"enableSwipeForTabs", "mainNavPosition", PrefKeys.HIDE_TOP_TOOLBAR, "viewPagerOffScreenLimit" -> {
restartActivitiesOnBackPressedCallback.isEnabled = true
}
"language" -> {
restartActivitiesOnBackPressedCallback.isEnabled = true
this.restartCurrentActivity()
}
}
eventHub.dispatch(PreferenceChangedEvent(key))

View File

@ -33,14 +33,13 @@ import com.keylesspalace.tusky.settings.makePreferenceScreen
import com.keylesspalace.tusky.settings.preference
import com.keylesspalace.tusky.settings.preferenceCategory
import com.keylesspalace.tusky.settings.switchPreference
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.LocaleManager
import com.keylesspalace.tusky.util.deserialize
import com.keylesspalace.tusky.util.getNonNullString
import com.keylesspalace.tusky.util.makeIcon
import com.keylesspalace.tusky.util.serialize
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizePx
import de.c1710.filemojicompat_ui.views.picker.preference.EmojiPickerPreference
import javax.inject.Inject
@ -49,6 +48,9 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
@Inject
lateinit var accountManager: AccountManager
@Inject
lateinit var localeManager: LocaleManager
private val iconSize by lazy { resources.getDimensionPixelSize(R.dimen.preference_icon_size) }
private var httpProxyPref: Preference? = null
@ -77,10 +79,11 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
setDefaultValue("default")
setEntries(R.array.language_entries)
setEntryValues(R.array.language_values)
key = PrefKeys.LANGUAGE
key = PrefKeys.LANGUAGE + "_" // deliberately not the actual key, the real handling happens in LocaleManager
setSummaryProvider { entry }
setTitle(R.string.pref_title_language)
icon = makeIcon(GoogleMaterial.Icon.gmd_translate)
preferenceDataStore = localeManager
}
listPreference {
@ -350,11 +353,7 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
}
private fun makeIcon(icon: GoogleMaterial.Icon): IconicsDrawable {
val context = requireContext()
return IconicsDrawable(context, icon).apply {
sizePx = iconSize
colorInt = ThemeUtils.getColor(context, R.attr.iconColor)
}
return makeIcon(requireContext(), icon, iconSize)
}
override fun onResume() {

View File

@ -22,12 +22,14 @@ import android.os.Bundle
import android.view.Menu
import androidx.activity.viewModels
import androidx.appcompat.widget.SearchView
import androidx.preference.PreferenceManager
import com.google.android.material.tabs.TabLayoutMediator
import com.keylesspalace.tusky.BottomSheetActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.search.adapter.SearchPagerAdapter
import com.keylesspalace.tusky.databinding.ActivitySearchBinding
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.viewBinding
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
@ -44,6 +46,8 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector {
private val binding by viewBinding(ActivitySearchBinding::inflate)
private val preferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
@ -61,6 +65,9 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector {
binding.pages.adapter = SearchPagerAdapter(this)
binding.pages.offscreenPageLimit = 4
val enableSwipeForTabs = preferences.getBoolean(PrefKeys.ENABLE_SWIPE_FOR_TABS, true)
binding.pages.isUserInputEnabled = enableSwipeForTabs
TabLayoutMediator(binding.tabs, binding.pages) {
tab, position ->
tab.text = getPageTitle(position)

View File

@ -23,6 +23,7 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.util.Log
import android.view.View
@ -438,13 +439,21 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
}
private fun requestDownloadAllMedia(status: Status) {
val permissions = arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
(activity as BaseActivity).requestPermissions(permissions) { _, grantResults ->
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
downloadAllMedia(status)
} else {
Toast.makeText(context, R.string.error_media_download_permission, Toast.LENGTH_SHORT).show()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
val permissions = arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
(activity as BaseActivity).requestPermissions(permissions) { _, grantResults ->
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
downloadAllMedia(status)
} else {
Toast.makeText(
context,
R.string.error_media_download_permission,
Toast.LENGTH_SHORT
).show()
}
}
} else {
downloadAllMedia(status)
}
}

View File

@ -77,6 +77,7 @@ fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity {
inReplyToAccountId = null,
content = null,
createdAt = 0L,
editedAt = 0L,
emojis = null,
reblogsCount = 0,
favouritesCount = 0,
@ -121,6 +122,7 @@ fun Status.toEntity(
inReplyToAccountId = actionableStatus.inReplyToAccountId,
content = actionableStatus.content,
createdAt = actionableStatus.createdAt.time,
editedAt = actionableStatus.editedAt?.time,
emojis = actionableStatus.emojis.let(gson::toJson),
reblogsCount = actionableStatus.reblogsCount,
favouritesCount = actionableStatus.favouritesCount,
@ -173,6 +175,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
reblog = null,
content = status.content.orEmpty(),
createdAt = Date(status.createdAt),
editedAt = status.editedAt?.let { Date(it) },
emojis = emojis,
reblogsCount = status.reblogsCount,
favouritesCount = status.favouritesCount,
@ -205,6 +208,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
reblog = reblog,
content = "",
createdAt = Date(status.createdAt), // lie but whatever?
editedAt = null,
emojis = listOf(),
reblogsCount = 0,
favouritesCount = 0,
@ -236,6 +240,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
reblog = null,
content = status.content.orEmpty(),
createdAt = Date(status.createdAt),
editedAt = status.editedAt?.let { Date(it) },
emojis = emojis,
reblogsCount = status.reblogsCount,
favouritesCount = status.favouritesCount,

View File

@ -44,6 +44,7 @@ import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.FilterModel
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.usecase.TimelineCases
import com.keylesspalace.tusky.util.EmptyPagingSource
import com.keylesspalace.tusky.viewdata.StatusViewData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
@ -89,7 +90,7 @@ class CachedTimelineViewModel @Inject constructor(
pagingSourceFactory = {
val activeAccount = accountManager.activeAccount
if (activeAccount == null) {
EmptyTimelinePagingSource()
EmptyPagingSource()
} else {
db.timelineDao().getStatuses(activeAccount.id)
}.also { newPagingSource ->

View File

@ -1,11 +0,0 @@
package com.keylesspalace.tusky.components.timeline.viewmodel
import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
class EmptyTimelinePagingSource : PagingSource<Int, TimelineStatusWithAccount>() {
override fun getRefreshKey(state: PagingState<Int, TimelineStatusWithAccount>): Int? = null
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, TimelineStatusWithAccount> = LoadResult.Page(emptyList(), null, null)
}

View File

@ -171,6 +171,12 @@ class ViewThreadFragment : SFragment(), OnRefreshListener, StatusActionListener,
}
}
is ThreadUiState.Success -> {
if (uiState.statuses.none { viewData -> viewData.isDetailed }) {
// no detailed statuses available, e.g. because author is blocked
activity?.finish()
return@collect
}
adapter.submitList(uiState.statuses) {
if (viewModel.isInitialLoad) {
viewModel.isInitialLoad = false

View File

@ -262,7 +262,7 @@ class ViewThreadViewModel @Inject constructor(
updateSuccess { uiState ->
uiState.copy(
statuses = uiState.statuses.filter { viewData ->
viewData.status.account.id == accountId
viewData.status.account.id != accountId
}
)
}
@ -366,11 +366,12 @@ class ViewThreadViewModel @Inject constructor(
}
private fun Status.toViewData(detailed: Boolean = false): StatusViewData.Concrete {
val oldStatus = (_uiState.value as? ThreadUiState.Success)?.statuses?.find { it.id == this.id }
return toViewData(
isShowingContent = alwaysShowSensitiveMedia || !actionableStatus.sensitive,
isExpanded = alwaysOpenSpoiler,
isCollapsed = !detailed,
isDetailed = detailed
isShowingContent = oldStatus?.isShowingContent ?: (alwaysShowSensitiveMedia || !actionableStatus.sensitive),
isExpanded = oldStatus?.isExpanded ?: alwaysOpenSpoiler,
isCollapsed = oldStatus?.isCollapsed ?: !detailed,
isDetailed = oldStatus?.isDetailed ?: detailed
)
}

View File

@ -54,11 +54,13 @@ data class AccountEntity(
var notificationsSubscriptions: Boolean = true,
var notificationsSignUps: Boolean = true,
var notificationsUpdates: Boolean = true,
var notificationsReports: Boolean = true,
var notificationSound: Boolean = true,
var notificationVibration: Boolean = true,
var notificationLight: Boolean = true,
var defaultPostPrivacy: Status.Visibility = Status.Visibility.PUBLIC,
var defaultMediaSensitivity: Boolean = false,
var defaultPostLanguage: String = "",
var alwaysShowSensitiveMedia: Boolean = false,
var alwaysOpenSpoiler: Boolean = false,
var mediaPreviewEnabled: Boolean = true,

View File

@ -154,6 +154,7 @@ class AccountManager @Inject constructor(db: AppDatabase) {
it.displayName = account.name
it.profilePictureUrl = account.avatar
it.defaultPostPrivacy = account.source?.privacy ?: Status.Visibility.PUBLIC
it.defaultPostLanguage = account.source?.language ?: ""
it.defaultMediaSensitivity = account.source?.sensitive ?: false
it.emojis = account.emojis ?: emptyList()

View File

@ -31,7 +31,7 @@ import java.io.File;
*/
@Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
TimelineAccountEntity.class, ConversationEntity.class
}, version = 42)
}, version = 45)
public abstract class AppDatabase extends RoomDatabase {
public abstract AccountDao accountDao();
@ -611,4 +611,26 @@ public abstract class AppDatabase extends RoomDatabase {
database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_language` TEXT");
}
};
public static final Migration MIGRATION_42_43 = new Migration(42, 43) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `defaultPostLanguage` TEXT NOT NULL DEFAULT ''");
}
};
public static final Migration MIGRATION_43_44 = new Migration(43, 44) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsReports` INTEGER NOT NULL DEFAULT 1");
}
};
public static final Migration MIGRATION_44_45 = new Migration(44, 45) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `editedAt` INTEGER");
database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_editedAt` INTEGER");
}
};
}

View File

@ -33,7 +33,7 @@ abstract class TimelineDao {
@Query(
"""
SELECT s.serverId, s.url, s.timelineUserId,
s.authorServerId, s.inReplyToId, s.inReplyToAccountId, s.createdAt,
s.authorServerId, s.inReplyToId, s.inReplyToAccountId, s.createdAt, s.editedAt,
s.emojis, s.reblogsCount, s.favouritesCount, s.repliesCount, s.reblogged, s.favourited, s.bookmarked, s.sensitive,
s.spoilerText, s.visibility, s.mentions, s.tags, s.application, s.reblogServerId,s.reblogAccountId,
s.content, s.attachments, s.poll, s.card, s.muted, s.expanded, s.contentShowing, s.contentCollapsed, s.pinned, s.language,

View File

@ -58,6 +58,7 @@ data class TimelineStatusEntity(
val inReplyToAccountId: String?,
val content: String?,
val createdAt: Long,
val editedAt: Long?,
val emojis: String?,
val reblogsCount: Int,
val favouritesCount: Int,

View File

@ -31,6 +31,7 @@ import com.keylesspalace.tusky.components.account.AccountActivity
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.drafts.DraftsActivity
import com.keylesspalace.tusky.components.followedtags.FollowedTagsActivity
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
import com.keylesspalace.tusky.components.login.LoginActivity
import com.keylesspalace.tusky.components.login.LoginWebViewActivity
@ -104,6 +105,9 @@ abstract class ActivitiesModule {
@ContributesAndroidInjector
abstract fun contributesFiltersActivity(): FiltersActivity
@ContributesAndroidInjector
abstract fun contributesFollowedTagsActivity(): FollowedTagsActivity
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
abstract fun contributesReportActivity(): ReportActivity

View File

@ -72,7 +72,8 @@ class AppModule {
AppDatabase.MIGRATION_32_33, AppDatabase.MIGRATION_33_34, AppDatabase.MIGRATION_34_35,
AppDatabase.MIGRATION_35_36, AppDatabase.MIGRATION_36_37, AppDatabase.MIGRATION_37_38,
AppDatabase.MIGRATION_38_39, AppDatabase.MIGRATION_39_40, AppDatabase.MIGRATION_40_41,
AppDatabase.MIGRATION_41_42,
AppDatabase.MIGRATION_41_42, AppDatabase.MIGRATION_42_43, AppDatabase.MIGRATION_43_44,
AppDatabase.MIGRATION_44_45,
)
.build()
}

View File

@ -16,6 +16,7 @@
package com.keylesspalace.tusky.di
import com.keylesspalace.tusky.AccountsInListFragment
import com.keylesspalace.tusky.components.account.list.ListsForAccountFragment
import com.keylesspalace.tusky.components.account.media.AccountMediaFragment
import com.keylesspalace.tusky.components.conversation.ConversationsFragment
import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment
@ -93,6 +94,9 @@ abstract class FragmentBuildersModule {
@ContributesAndroidInjector
abstract fun preferencesFragment(): PreferencesFragment
@ContributesAndroidInjector
abstract fun listsForAccountFragment(): ListsForAccountFragment
@ContributesAndroidInjector
abstract fun searchNotestockFragment(): SearchNotestockFragment
}

View File

@ -5,11 +5,13 @@ package com.keylesspalace.tusky.di
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.keylesspalace.tusky.components.account.AccountViewModel
import com.keylesspalace.tusky.components.account.list.ListsForAccountViewModel
import com.keylesspalace.tusky.components.account.media.AccountMediaViewModel
import com.keylesspalace.tusky.components.announcements.AnnouncementsViewModel
import com.keylesspalace.tusky.components.compose.ComposeViewModel
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
import com.keylesspalace.tusky.components.drafts.DraftsViewModel
import com.keylesspalace.tusky.components.followedtags.FollowedTagsViewModel
import com.keylesspalace.tusky.components.login.LoginWebViewViewModel
import com.keylesspalace.tusky.components.report.ReportViewModel
import com.keylesspalace.tusky.components.scheduled.ScheduledStatusViewModel
@ -127,6 +129,16 @@ abstract class ViewModelModule {
@ViewModelKey(LoginWebViewViewModel::class)
internal abstract fun loginWebViewViewModel(viewModel: LoginWebViewViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(FollowedTagsViewModel::class)
internal abstract fun followedTagsViewModel(viewModel: FollowedTagsViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(ListsForAccountViewModel::class)
internal abstract fun listsForAccountViewModel(viewModel: ListsForAccountViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(QuickTootViewModel::class)

View File

@ -59,7 +59,8 @@ data class AccountSource(
val privacy: Status.Visibility?,
val sensitive: Boolean?,
val note: String?,
val fields: List<StringField>?
val fields: List<StringField>?,
val language: String?,
)
data class Field(

View File

@ -68,7 +68,9 @@ data class Attachment(
@Parcelize
data class MetaData(
val focus: Focus?,
val duration: Float?
val duration: Float?,
val original: Size?,
val small: Size?,
) : Parcelable
/**
@ -82,4 +84,14 @@ data class Attachment(
val x: Float,
val y: Float
) : Parcelable
/**
* The size of an image, used to specify the width/height.
*/
@Parcelize
data class Size(
val width: Int,
val height: Int,
val aspect: Double
) : Parcelable
}

View File

@ -25,7 +25,8 @@ data class Notification(
val type: Type,
val id: String,
val account: TimelineAccount,
val status: Status?
val status: Status?,
val report: Report?,
) {
@JsonAdapter(NotificationTypeAdapter::class)
@ -40,6 +41,7 @@ data class Notification(
STATUS("status"),
SIGN_UP("admin.sign_up"),
UPDATE("update"),
REPORT("admin.report"),
;
companion object {
@ -52,7 +54,7 @@ data class Notification(
}
return UNKNOWN
}
val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL, STATUS, SIGN_UP, UPDATE)
val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL, STATUS, SIGN_UP, UPDATE, REPORT)
}
override fun toString(): String {

View File

@ -0,0 +1,12 @@
package com.keylesspalace.tusky.entity
import com.google.gson.annotations.SerializedName
import java.util.Date
data class Report(
val id: String,
val category: String,
val status_ids: List<String>?,
@SerializedName("created_at") val createdAt: Date,
@SerializedName("target_account") val targetAccount: TimelineAccount,
)

View File

@ -30,6 +30,7 @@ data class Status(
val reblog: Status?,
val content: String,
@SerializedName("created_at", alternate = ["published"]) val createdAt: Date,
@SerializedName("edited_at") val editedAt: Date?,
val emojis: List<Emoji>,
@SerializedName("reblogs_count") val reblogsCount: Int,
@SerializedName("favourites_count") val favouritesCount: Int,

View File

@ -31,10 +31,8 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.PopupWindow;
import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -67,6 +65,7 @@ import com.keylesspalace.tusky.appstore.PinEvent;
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent;
import com.keylesspalace.tusky.appstore.QuickReplyEvent;
import com.keylesspalace.tusky.appstore.ReblogEvent;
import com.keylesspalace.tusky.databinding.FragmentTimelineNotificationsBinding;
import com.keylesspalace.tusky.db.AccountEntity;
import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.di.Injectable;
@ -82,13 +81,13 @@ import com.keylesspalace.tusky.settings.PrefKeys;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.Either;
import com.keylesspalace.tusky.util.HttpHeaderLink;
import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
import com.keylesspalace.tusky.util.ListUtils;
import com.keylesspalace.tusky.util.NotificationTypeConverterKt;
import com.keylesspalace.tusky.util.PairedList;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.util.ViewDataUtils;
import com.keylesspalace.tusky.view.BackgroundMessageView;
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
import com.keylesspalace.tusky.viewdata.AttachmentViewData;
import com.keylesspalace.tusky.viewdata.NotificationViewData;
@ -160,16 +159,11 @@ public class NotificationsFragment extends SFragment implements
@Inject
EventHub eventHub;
private SwipeRefreshLayout swipeRefreshLayout;
private RecyclerView recyclerView;
private ProgressBar progressBar;
private BackgroundMessageView statusView;
private AppBarLayout appBarOptions;
private FragmentTimelineNotificationsBinding binding;
private LinearLayoutManager layoutManager;
private EndlessOnScrollListener scrollListener;
private NotificationsAdapter adapter;
private Button buttonFilter;
private boolean hideFab;
private boolean topLoading;
private boolean bottomLoading;
@ -213,35 +207,29 @@ public class NotificationsFragment extends SFragment implements
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_timeline_notifications, container, false);
binding = FragmentTimelineNotificationsBinding.inflate(inflater, container, false);
@NonNull Context context = inflater.getContext(); // from inflater to silence warning
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(requireContext());
boolean showNotificationsFilterSetting = preferences.getBoolean("showNotificationsFilter", true);
//Clear notifications on filter visibility change to force refresh
// Clear notifications on filter visibility change to force refresh
if (showNotificationsFilterSetting != showNotificationsFilter)
notifications.clear();
showNotificationsFilter = showNotificationsFilterSetting;
// Setup the SwipeRefreshLayout.
swipeRefreshLayout = rootView.findViewById(R.id.swipeRefreshLayout);
recyclerView = rootView.findViewById(R.id.recyclerView);
progressBar = rootView.findViewById(R.id.progressBar);
statusView = rootView.findViewById(R.id.statusView);
appBarOptions = rootView.findViewById(R.id.appBarOptions);
swipeRefreshLayout.setOnRefreshListener(this);
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue);
binding.swipeRefreshLayout.setOnRefreshListener(this);
binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue);
loadNotificationsFilter();
// Setup the RecyclerView.
recyclerView.setHasFixedSize(true);
binding.recyclerView.setHasFixedSize(true);
layoutManager = new LinearLayoutManager(context);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAccessibilityDelegateCompat(
new ListStatusAccessibilityDelegate(recyclerView, this, (pos) -> {
binding.recyclerView.setLayoutManager(layoutManager);
binding.recyclerView.setAccessibilityDelegateCompat(
new ListStatusAccessibilityDelegate(binding.recyclerView, this, (pos) -> {
NotificationViewData notification = notifications.getPairedItemOrNull(pos);
// We support replies only for now
if (notification instanceof NotificationViewData.Concrete) {
@ -251,7 +239,7 @@ public class NotificationsFragment extends SFragment implements
}
}));
recyclerView.addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.VERTICAL));
binding.recyclerView.addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.VERTICAL));
StatusDisplayOptions statusDisplayOptions = new StatusDisplayOptions(
preferences.getBoolean("animateGifAvatars", false),
@ -271,7 +259,7 @@ public class NotificationsFragment extends SFragment implements
dataSource, statusDisplayOptions, this, this, this);
alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia();
alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler();
recyclerView.setAdapter(adapter);
binding.recyclerView.setAdapter(adapter);
topLoading = false;
bottomLoading = false;
@ -279,43 +267,47 @@ public class NotificationsFragment extends SFragment implements
updateAdapter();
Button buttonClear = rootView.findViewById(R.id.buttonClear);
buttonClear.setOnClickListener(v -> confirmClearNotifications());
buttonFilter = rootView.findViewById(R.id.buttonFilter);
buttonFilter.setOnClickListener(v -> showFilterMenu());
binding.buttonClear.setOnClickListener(v -> confirmClearNotifications());
binding.buttonFilter.setOnClickListener(v -> showFilterMenu());
if (notifications.isEmpty()) {
swipeRefreshLayout.setEnabled(false);
binding.swipeRefreshLayout.setEnabled(false);
sendFetchNotificationsRequest(null, null, FetchEnd.BOTTOM, -1);
} else {
progressBar.setVisibility(View.GONE);
binding.progressBar.setVisibility(View.GONE);
}
((SimpleItemAnimator) recyclerView.getItemAnimator()).setSupportsChangeAnimations(false);
((SimpleItemAnimator) binding.recyclerView.getItemAnimator()).setSupportsChangeAnimations(false);
updateFilterVisibility();
return rootView;
return binding.getRoot();
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
private void updateFilterVisibility() {
CoordinatorLayout.LayoutParams params =
(CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams();
(CoordinatorLayout.LayoutParams) binding.swipeRefreshLayout.getLayoutParams();
if (showNotificationsFilter && !showingError) {
appBarOptions.setExpanded(true, false);
appBarOptions.setVisibility(View.VISIBLE);
//Set content behaviour to hide filter on scroll
binding.appBarOptions.setExpanded(true, false);
binding.appBarOptions.setVisibility(View.VISIBLE);
// Set content behaviour to hide filter on scroll
params.setBehavior(new AppBarLayout.ScrollingViewBehavior());
} else {
appBarOptions.setExpanded(false, false);
appBarOptions.setVisibility(View.GONE);
//Clear behaviour to hide app bar
binding.appBarOptions.setExpanded(false, false);
binding.appBarOptions.setVisibility(View.GONE);
// Clear behaviour to hide app bar
params.setBehavior(null);
}
}
private void confirmClearNotifications() {
new AlertDialog.Builder(getContext())
new AlertDialog.Builder(requireContext())
.setMessage(R.string.notification_clear_text)
.setPositiveButton(android.R.string.ok, (DialogInterface dia, int which) -> clearNotifications())
.setNegativeButton(android.R.string.cancel, null)
@ -328,10 +320,10 @@ public class NotificationsFragment extends SFragment implements
Activity activity = getActivity();
if (activity == null) throw new AssertionError("Activity is null");
/* This is delayed until onActivityCreated solely because MainActivity.composeButton isn't
* guaranteed to be set until then.
* Use a modified scroll listener that both loads more notificationsEnabled as it goes, and hides
* the compose button on down-scroll. */
// This is delayed until onActivityCreated solely because MainActivity.composeButton
// isn't guaranteed to be set until then.
// Use a modified scroll listener that both loads more notificationsEnabled as it
// goes, and hides the compose button on down-scroll.
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
hideFab = preferences.getBoolean("fabHide", false);
scrollListener = new EndlessOnScrollListener(layoutManager) {
@ -345,9 +337,9 @@ public class NotificationsFragment extends SFragment implements
if (composeButton != null) {
if (hideFab) {
if (dy > 0 && composeButton.isShown()) {
composeButton.hide(); // hides the button if we're scrolling down
composeButton.hide(); // Hides the button if we're scrolling down
} else if (dy < 0 && !composeButton.isShown()) {
composeButton.show(); // shows it if we are scrolling up
composeButton.show(); // Shows it if we are scrolling up
}
} else if (!composeButton.isShown()) {
composeButton.show();
@ -361,7 +353,7 @@ public class NotificationsFragment extends SFragment implements
}
};
recyclerView.addOnScrollListener(scrollListener);
binding.recyclerView.addOnScrollListener(scrollListener);
eventHub.getEvents()
.observeOn(AndroidSchedulers.mainThread())
@ -385,7 +377,7 @@ public class NotificationsFragment extends SFragment implements
@Override
public void onRefresh() {
this.statusView.setVisibility(View.GONE);
binding.statusView.setVisibility(View.GONE);
this.showingError = false;
Either<Placeholder, Notification> first = CollectionsKt.firstOrNull(this.notifications);
String topId;
@ -526,7 +518,7 @@ public class NotificationsFragment extends SFragment implements
@Override
public void onLoadMore(int position) {
//check bounds before accessing list,
// Check bounds before accessing list,
if (notifications.size() >= position && position > 0) {
Notification previous = notifications.get(position - 1).asRightOrNull();
Notification next = notifications.get(position + 1).asRightOrNull();
@ -548,7 +540,6 @@ public class NotificationsFragment extends SFragment implements
@Override
public void onContentCollapsedChange(boolean isCollapsed, int position) {
updateViewDataAt(position, (vd) -> vd.copyWithCollapsed(isCollapsed));
;
}
private void updateStatus(String statusId, Function<Status, Status> mapper) {
@ -623,28 +614,28 @@ public class NotificationsFragment extends SFragment implements
}
private void clearNotifications() {
//Cancel all ongoing requests
swipeRefreshLayout.setRefreshing(false);
// Cancel all ongoing requests
binding.swipeRefreshLayout.setRefreshing(false);
resetNotificationsLoad();
//Show friend elephant
this.statusView.setVisibility(View.VISIBLE);
this.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null);
// Show friend elephant
binding.statusView.setVisibility(View.VISIBLE);
binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null);
updateFilterVisibility();
//Update adapter
// Update adapter
updateAdapter();
//Execute clear notifications request
// Execute clear notifications request
mastodonApi.clearNotifications()
.observeOn(AndroidSchedulers.mainThread())
.to(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
.subscribe(
response -> {
// nothing to do
// Nothing to do
},
throwable -> {
//Reload notifications on failure
// Reload notifications on failure
fullyRefreshWithProgressBar(true);
});
}
@ -654,10 +645,10 @@ public class NotificationsFragment extends SFragment implements
bottomLoading = false;
topLoading = false;
//Disable load more
// Disable load more
bottomId = null;
//Clear exists notifications
// Clear exists notifications
notifications.clear();
}
@ -696,7 +687,7 @@ public class NotificationsFragment extends SFragment implements
window.setFocusable(true);
window.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
window.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
window.showAsDropDown(buttonFilter);
window.showAsDropDown(binding.buttonFilter);
}
@ -720,6 +711,8 @@ public class NotificationsFragment extends SFragment implements
return getString(R.string.notification_sign_up_name);
case UPDATE:
return getString(R.string.notification_update_name);
case REPORT:
return getString(R.string.notification_report_name);
default:
return "Unknown";
}
@ -762,12 +755,12 @@ public class NotificationsFragment extends SFragment implements
}
@Override
public void onViewTag(String tag) {
public void onViewTag(@NonNull String tag) {
super.viewTag(tag);
}
@Override
public void onViewAccount(String id) {
public void onViewAccount(@NonNull String id) {
super.viewAccount(id);
}
@ -809,10 +802,15 @@ public class NotificationsFragment extends SFragment implements
Log.w(TAG, "Didn't find a notification for ID: " + notificationId);
}
@Override
public void onViewReport(String reportId) {
LinkHelper.openLink(requireContext(), String.format("https://%s/admin/reports/%s", accountManager.getActiveAccount().getDomain(), reportId));
}
private void onPreferenceChanged(String key) {
switch (key) {
case "fabHide": {
hideFab = PreferenceManager.getDefaultSharedPreferences(getContext()).getBoolean("fabHide", false);
hideFab = PreferenceManager.getDefaultSharedPreferences(requireContext()).getBoolean("fabHide", false);
break;
}
case "mediaPreviewEnabled": {
@ -825,7 +823,7 @@ public class NotificationsFragment extends SFragment implements
}
case "showNotificationsFilter": {
if (isAdded()) {
showNotificationsFilter = PreferenceManager.getDefaultSharedPreferences(getContext()).getBoolean("showNotificationsFilter", true);
showNotificationsFilter = PreferenceManager.getDefaultSharedPreferences(requireContext()).getBoolean("showNotificationsFilter", true);
updateFilterVisibility();
fullyRefreshWithProgressBar(true);
}
@ -841,7 +839,7 @@ public class NotificationsFragment extends SFragment implements
}
private void removeAllByAccountId(String accountId) {
// using iterator to safely remove items while iterating
// Using iterator to safely remove items while iterating
Iterator<Either<Placeholder, Notification>> iterator = notifications.iterator();
while (iterator.hasNext()) {
Either<Placeholder, Notification> notification = iterator.next();
@ -855,7 +853,7 @@ public class NotificationsFragment extends SFragment implements
private void onLoadMore() {
if (bottomId == null) {
// already loaded everything
// Already loaded everything
return;
}
@ -885,7 +883,7 @@ public class NotificationsFragment extends SFragment implements
private void jumpToTop() {
if (isAdded()) {
appBarOptions.setExpanded(true, false);
binding.appBarOptions.setExpanded(true, false);
layoutManager.scrollToPosition(0);
scrollListener.reset();
}
@ -893,8 +891,8 @@ public class NotificationsFragment extends SFragment implements
private void sendFetchNotificationsRequest(String fromId, String uptoId,
final FetchEnd fetchEnd, final int pos) {
/* If there is a fetch already ongoing, record however many fetches are requested and
* fulfill them after it's complete. */
// If there is a fetch already ongoing, record however many fetches are requested and
// fulfill them after it's complete.
if (fetchEnd == FetchEnd.TOP && topLoading) {
return;
}
@ -970,18 +968,18 @@ public class NotificationsFragment extends SFragment implements
}
if (notifications.size() == 0 && adapter.getItemCount() == 0) {
this.statusView.setVisibility(View.VISIBLE);
this.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null);
binding.statusView.setVisibility(View.VISIBLE);
binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null);
}
updateFilterVisibility();
swipeRefreshLayout.setEnabled(true);
swipeRefreshLayout.setRefreshing(false);
progressBar.setVisibility(View.GONE);
binding.swipeRefreshLayout.setEnabled(true);
binding.swipeRefreshLayout.setRefreshing(false);
binding.progressBar.setVisibility(View.GONE);
}
private void onFetchNotificationsFailure(Throwable throwable, FetchEnd fetchEnd, int position) {
swipeRefreshLayout.setRefreshing(false);
binding.swipeRefreshLayout.setRefreshing(false);
if (fetchEnd == FetchEnd.MIDDLE && !notifications.get(position).isRight()) {
Placeholder placeholder = notifications.get(position).asLeft();
NotificationViewData placeholderVD =
@ -989,18 +987,18 @@ public class NotificationsFragment extends SFragment implements
notifications.setPairedItem(position, placeholderVD);
updateAdapter();
} else if (this.notifications.isEmpty()) {
this.statusView.setVisibility(View.VISIBLE);
swipeRefreshLayout.setEnabled(false);
binding.statusView.setVisibility(View.VISIBLE);
binding.swipeRefreshLayout.setEnabled(false);
this.showingError = true;
if (throwable instanceof IOException) {
this.statusView.setup(R.drawable.elephant_offline, R.string.error_network, __ -> {
this.progressBar.setVisibility(View.VISIBLE);
binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network, __ -> {
binding.progressBar.setVisibility(View.VISIBLE);
this.onRefresh();
return Unit.INSTANCE;
});
} else {
this.statusView.setup(R.drawable.elephant_error, R.string.error_generic, __ -> {
this.progressBar.setVisibility(View.VISIBLE);
binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic, __ -> {
binding.progressBar.setVisibility(View.VISIBLE);
this.onRefresh();
return Unit.INSTANCE;
});
@ -1016,7 +1014,7 @@ public class NotificationsFragment extends SFragment implements
bottomLoading = false;
}
progressBar.setVisibility(View.GONE);
binding.progressBar.setVisibility(View.GONE);
}
private void saveNewestNotificationId(List<Notification> notifications) {
@ -1053,8 +1051,8 @@ public class NotificationsFragment extends SFragment implements
notifications.addAll(liftedNew);
} else {
int index = notifications.indexOf(liftedNew.get(newNotifications.size() - 1));
for (int i = 0; i < index; i++) {
notifications.remove(0);
if (index > 0) {
notifications.subList(0, index).clear();
}
int newIndex = liftedNew.indexOf(notifications.get(0));
@ -1078,7 +1076,7 @@ public class NotificationsFragment extends SFragment implements
int end = notifications.size();
List<Either<Placeholder, Notification>> liftedNew = liftNotificationList(newNotifications);
Either<Placeholder, Notification> last = notifications.get(end - 1);
if (last != null && liftedNew.indexOf(last) == -1) {
if (last != null && !liftedNew.contains(last)) {
notifications.addAll(liftedNew);
updateAdapter();
}
@ -1116,8 +1114,8 @@ public class NotificationsFragment extends SFragment implements
private void fullyRefreshWithProgressBar(boolean isShow) {
resetNotificationsLoad();
if (isShow) {
progressBar.setVisibility(View.VISIBLE);
statusView.setVisibility(View.GONE);
binding.progressBar.setVisibility(View.VISIBLE);
binding.statusView.setVisibility(View.GONE);
}
updateAdapter();
sendFetchNotificationsRequest(null, null, FetchEnd.TOP, -1);
@ -1156,7 +1154,7 @@ public class NotificationsFragment extends SFragment implements
// scroll up when new items at the top are loaded while being at the start
// https://github.com/tuskyapp/Tusky/pull/1905#issuecomment-677819724
if (position == 0 && context != null && adapter.getItemCount() != count) {
recyclerView.scrollBy(0, Utils.dpToPx(context, -30));
binding.recyclerView.scrollBy(0, Utils.dpToPx(context, -30));
}
}
}
@ -1211,7 +1209,7 @@ public class NotificationsFragment extends SFragment implements
@Override
public Object getChangePayload(@NonNull NotificationViewData oldItem, @NonNull NotificationViewData newItem) {
if (oldItem.deepEquals(newItem)) {
//If items are equal - update timestamp only
// If items are equal - update timestamp only
return Collections.singletonList(StatusBaseViewHolder.Key.KEY_CREATED);
} else
// If items are different - update a whole view holder
@ -1237,7 +1235,7 @@ public class NotificationsFragment extends SFragment implements
* Auto dispose observable on pause
*/
private void startUpdateTimestamp() {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(requireContext());
boolean useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false);
if (!useAbsoluteTime) {
Observable.interval(0, 1, TimeUnit.MINUTES)

View File

@ -25,6 +25,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.util.Log;
import android.view.Menu;
@ -501,13 +502,17 @@ public abstract class SFragment extends Fragment implements Injectable {
}
private void requestDownloadAllMedia(Status status) {
String[] permissions = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE};
((BaseActivity) getActivity()).requestPermissions(permissions, (permissions1, grantResults) -> {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
downloadAllMedia(status);
} else {
Toast.makeText(getContext(), R.string.error_media_download_permission, Toast.LENGTH_SHORT).show();
}
});
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
String[] permissions = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE};
((BaseActivity) getActivity()).requestPermissions(permissions, (permissions1, grantResults) -> {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
downloadAllMedia(status);
} else {
Toast.makeText(getContext(), R.string.error_media_download_permission, Toast.LENGTH_SHORT).show();
}
});
} else {
downloadAllMedia(status);
}
}
}

View File

@ -18,27 +18,36 @@ package com.keylesspalace.tusky.fragment
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.method.ScrollingMovementMethod
import android.view.GestureDetector
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.MediaController
import androidx.core.view.GestureDetectorCompat
import com.keylesspalace.tusky.ViewMediaActivity
import com.keylesspalace.tusky.databinding.FragmentViewVideoBinding
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.view.ExposedPlayPauseVideoView
import kotlin.math.abs
class ViewVideoFragment : ViewMediaFragment() {
interface VideoActionsListener {
fun onDismiss()
}
private var _binding: FragmentViewVideoBinding? = null
private val binding get() = _binding!!
private lateinit var videoActionsListener: VideoActionsListener
private lateinit var toolbar: View
private val handler = Handler(Looper.getMainLooper())
private val hideToolbar = Runnable {
@ -52,6 +61,11 @@ class ViewVideoFragment : ViewMediaFragment() {
private lateinit var mediaController: MediaController
private var isAudio = false
override fun onAttach(context: Context) {
super.onAttach(context)
videoActionsListener = context as VideoActionsListener
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
// Start/pause/resume video playback as fragment is shown/hidden
super.setUserVisibleHint(isVisibleToUser)
@ -168,6 +182,7 @@ class ViewVideoFragment : ViewMediaFragment() {
return binding.root
}
@SuppressLint("ClickableViewAccessibility")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val attachment = arguments?.getParcelable<Attachment>(ARG_ATTACHMENT)
@ -177,6 +192,54 @@ class ViewVideoFragment : ViewMediaFragment() {
}
val url = attachment.url
isAudio = attachment.type == Attachment.Type.AUDIO
val gestureDetector = GestureDetectorCompat(
requireContext(),
object : GestureDetector.SimpleOnGestureListener() {
override fun onDown(event: MotionEvent): Boolean {
return true
}
override fun onFling(
e1: MotionEvent,
e2: MotionEvent,
velocityX: Float,
velocityY: Float
): Boolean {
if (abs(velocityY) > abs(velocityX)) {
videoActionsListener.onDismiss()
return true
}
return false
}
}
)
var lastY = 0f
binding.root.setOnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_DOWN) {
lastY = event.rawY
} else if (event.pointerCount == 1 && event.action == MotionEvent.ACTION_MOVE) {
val diff = event.rawY - lastY
if (binding.videoView.translationY != 0f || abs(diff) > 40) {
binding.videoView.translationY += diff
val scale = (-abs(binding.videoView.translationY) / 720 + 1).coerceAtLeast(0.5f)
binding.videoView.scaleY = scale
binding.videoView.scaleX = scale
lastY = event.rawY
return@setOnTouchListener true
}
} else if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) {
if (abs(binding.videoView.translationY) > 180) {
videoActionsListener.onDismiss()
} else {
binding.videoView.animate().translationY(0f).scaleX(1f).scaleY(1f).start()
}
}
gestureDetector.onTouchEvent(event)
}
finalizeViewSetup(url, attachment.previewUrl, attachment.description)
}

View File

@ -0,0 +1,5 @@
package com.keylesspalace.tusky.interfaces
interface HashtagActionListener {
fun unfollow(tagName: String, position: Int)
}

View File

@ -267,7 +267,8 @@ interface MastodonApi {
@PATCH("api/v1/accounts/update_credentials")
fun accountUpdateSource(
@Field("source[privacy]") privacy: String?,
@Field("source[sensitive]") sensitive: Boolean?
@Field("source[sensitive]") sensitive: Boolean?,
@Field("source[language]") language: String?,
): Call<Account>
@Multipart
@ -481,6 +482,11 @@ interface MastodonApi {
@GET("/api/v1/lists")
suspend fun getLists(): NetworkResult<List<MastoList>>
@GET("/api/v1/accounts/{id}/lists")
suspend fun getListsIncludesAccount(
@Path("id") accountId: String
): NetworkResult<List<MastoList>>
@FormUrlEncoded
@POST("api/v1/lists")
suspend fun createList(
@ -668,6 +674,14 @@ interface MastodonApi {
@GET("api/v1/tags/{name}")
suspend fun tag(@Path("name") name: String): NetworkResult<HashTag>
@GET("api/v1/followed_tags")
suspend fun followedTags(
@Query("min_id") minId: String? = null,
@Query("since_id") sinceId: String? = null,
@Query("max_id") maxId: String? = null,
@Query("limit") limit: Int? = null,
): Response<List<HashTag>>
@POST("api/v1/tags/{name}/follow")
suspend fun followTag(@Path("name") name: String): NetworkResult<HashTag>

View File

@ -55,6 +55,7 @@ object PrefKeys {
const val STACK_TRACE = "stackTrace"
const val DEFAULT_POST_PRIVACY = "defaultPostPrivacy"
const val DEFAULT_POST_LANGUAGE = "defaultPostLanguage"
const val DEFAULT_MEDIA_SENSITIVITY = "defaultMediaSensitivity"
const val MEDIA_PREVIEW_ENABLED = "mediaPreviewEnabled"
const val ALWAYS_SHOW_SENSITIVE_MEDIA = "alwaysShowSensitiveMedia"
@ -72,6 +73,7 @@ object PrefKeys {
const val NOTIFICATION_FILTER_SUBSCRIPTIONS = "notificationFilterSubscriptions"
const val NOTIFICATION_FILTER_SIGN_UPS = "notificationFilterSignUps"
const val NOTIFICATION_FILTER_UPDATES = "notificationFilterUpdates"
const val NOTIFICATION_FILTER_REPORTS = "notificationFilterReports"
const val TAB_FILTER_HOME_REPLIES = "tabFilterHomeReplies_v2" // This was changed once to reset an unintentionally set default.
const val TAB_FILTER_HOME_BOOSTS = "tabFilterHomeBoosts"

View File

@ -1,4 +1,5 @@
@file:JvmName("AttachmentHelper")
package com.keylesspalace.tusky.util
import android.content.Context
@ -24,3 +25,12 @@ private fun formatDuration(durationInSeconds: Double): String {
val hours = durationInSeconds.toInt() / 3600
return "%d:%02d:%02d".format(hours, minutes, seconds)
}
fun List<Attachment>.aspectRatios(): List<Double> {
return map { attachment ->
// clamp ratio between 2:1 & 1:2, defaulting to 16:9
val size = (attachment.meta?.small ?: attachment.meta?.original) ?: return@map 1.7778
val aspect = if (size.aspect > 0) size.aspect else size.width.toDouble() / size.height
aspect.coerceIn(0.5, 2.0)
}
}

View File

@ -0,0 +1,10 @@
package com.keylesspalace.tusky.util
import androidx.paging.PagingSource
import androidx.paging.PagingState
class EmptyPagingSource<T : Any> : PagingSource<Int, T>() {
override fun getRefreshKey(state: PagingState<Int, T>): Int? = null
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> = LoadResult.Page(emptyList(), null, null)
}

View File

@ -0,0 +1,31 @@
/* Copyright 2022 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.util
import android.content.Context
import androidx.annotation.Px
import com.keylesspalace.tusky.R
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizePx
fun makeIcon(context: Context, icon: GoogleMaterial.Icon, @Px iconSize: Int): IconicsDrawable {
return IconicsDrawable(context, icon).apply {
sizePx = iconSize
colorInt = ThemeUtils.getColor(context, R.attr.iconColor)
}
}

View File

@ -37,6 +37,8 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.HashTag
import com.keylesspalace.tusky.entity.Status.Mention
import com.keylesspalace.tusky.interfaces.LinkListener
import java.net.URI
import java.net.URISyntaxException
fun getDomain(urlString: String?): String {
val host = urlString?.toUri()?.host
@ -72,22 +74,16 @@ fun markupHiddenUrls(context: Context, content: CharSequence): SpannableStringBu
val spannableContent = SpannableStringBuilder(content)
val originalSpans = spannableContent.getSpans(0, content.length, URLSpan::class.java)
val obscuredLinkSpans = originalSpans.filter {
val text = spannableContent.subSequence(spannableContent.getSpanStart(it), spannableContent.getSpanEnd(it))
val firstCharacter = text[0]
val start = spannableContent.getSpanStart(it)
val firstCharacter = content[start]
return@filter if (firstCharacter == '#' || firstCharacter == '@') {
false
} else {
val lastPart = text.toString().split(' ').lastOrNull() ?: ""
var textDomain = getDomain(lastPart)
val text = spannableContent.subSequence(start, spannableContent.getSpanEnd(it)).toString()
.split(' ').lastOrNull() ?: ""
var textDomain = getDomain(text)
if (textDomain.isBlank()) {
// Allow "some.domain" or "www.some.domain" without a domain notifier
textDomain = lastPart
if (textDomain.startsWith("www.")) {
textDomain = textDomain.substring(4)
}
if (textDomain.endsWith("/")) {
textDomain = textDomain.substring(0, textDomain.length - 1)
}
textDomain = getDomain("https://$text")
}
getDomain(it.url) != textDomain
}
@ -276,4 +272,49 @@ private fun openLinkInCustomTab(uri: Uri, context: Context) {
}
}
// https://mastodon.foo.bar/@User
// https://mastodon.foo.bar/@User/43456787654678
// https://pleroma.foo.bar/users/User
// https://pleroma.foo.bar/users/9qTHT2ANWUdXzENqC0
// https://pleroma.foo.bar/notice/9sBHWIlwwGZi5QGlHc
// https://pleroma.foo.bar/objects/d4643c42-3ae0-4b73-b8b0-c725f5819207
// https://friendica.foo.bar/profile/user
// https://friendica.foo.bar/display/d4643c42-3ae0-4b73-b8b0-c725f5819207
// https://misskey.foo.bar/notes/83w6r388br (always lowercase)
// https://pixelfed.social/p/connyduck/391263492998670833
// https://pixelfed.social/connyduck
// https://gts.foo.bar/@goblin/statuses/01GH9XANCJ0TA8Y95VE9H3Y0Q2
// https://gts.foo.bar/@goblin
// https://foo.microblog.pub/o/5b64045effd24f48a27d7059f6cb38f5
fun looksLikeMastodonUrl(urlString: String): Boolean {
val uri: URI
try {
uri = URI(urlString)
} catch (e: URISyntaxException) {
return false
}
if (uri.query != null ||
uri.fragment != null ||
uri.path == null
) {
return false
}
return uri.path.let {
it.matches("^/@[^/]+$".toRegex()) ||
it.matches("^/@[^/]+/\\d+$".toRegex()) ||
it.matches("^/users/\\w+$".toRegex()) ||
it.matches("^/notice/[a-zA-Z0-9]+$".toRegex()) ||
it.matches("^/objects/[-a-f0-9]+$".toRegex()) ||
it.matches("^/notes/[a-z0-9]+$".toRegex()) ||
it.matches("^/display/[-a-f0-9]+$".toRegex()) ||
it.matches("^/profile/\\w+$".toRegex()) ||
it.matches("^/p/\\w+/\\d+$".toRegex()) ||
it.matches("^/\\w+$".toRegex()) ||
it.matches("^/@[^/]+/statuses/[a-zA-Z0-9]+$".toRegex()) ||
it.matches("^/o/[a-f0-9]+$".toRegex())
}
}
private const val TAG = "LinkHelper"

View File

@ -0,0 +1,36 @@
/* Copyright 2022 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.util
import android.content.Context
import com.keylesspalace.tusky.R
import java.util.Locale
// When a language code has changed, `language` *explicitly* returns the obsolete version,
// but `toLanguageTag()` uses the current version
// https://developer.android.com/reference/java/util/Locale#getLanguage()
val Locale.modernLanguageCode: String
get() {
return this.toLanguageTag().split('-', limit = 2)[0]
}
fun Locale.getTuskyDisplayName(context: Context): String {
return context.getString(
R.string.language_display_name_format,
this?.displayLanguage,
this?.getDisplayLanguage(this)
)
}

View File

@ -17,25 +17,89 @@ package com.keylesspalace.tusky.util
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Configuration
import android.os.Build
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import androidx.preference.PreferenceDataStore
import androidx.preference.PreferenceManager
import java.util.Locale
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.settings.PrefKeys
import javax.inject.Inject
import javax.inject.Singleton
class LocaleManager(context: Context) {
@Singleton
class LocaleManager @Inject constructor(
val context: Context
) : PreferenceDataStore() {
private var prefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
fun setLocale(context: Context): Context {
val language = prefs.getNonNullString("language", "default")
if (language == "default") {
return context
}
val locale = Locale.forLanguageTag(language)
Locale.setDefault(locale)
fun setLocale() {
val language = prefs.getNonNullString(PrefKeys.LANGUAGE, DEFAULT)
val res = context.resources
val config = Configuration(res.configuration)
config.setLocale(locale)
return context.createConfigurationContext(config)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (language != HANDLED_BY_SYSTEM) {
// app is being opened on Android 13+ for the first time
// hand over the old setting to the system and save a dummy value in Shared Preferences
applyLanguageToApp(language)
prefs.edit()
.putString(PrefKeys.LANGUAGE, HANDLED_BY_SYSTEM)
.apply()
}
} else {
// on Android < 13 we have to apply the language at every app start
applyLanguageToApp(language)
}
}
override fun putString(key: String?, value: String?) {
// if we are on Android < 13 we have to save the selected language so we can apply it at appstart
// on Android 13+ the system handles it for us
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
prefs.edit()
.putString(PrefKeys.LANGUAGE, value)
.apply()
}
applyLanguageToApp(value)
}
override fun getString(key: String?, defValue: String?): String? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val selectedLanguage = AppCompatDelegate.getApplicationLocales()
if (selectedLanguage.isEmpty) {
DEFAULT
} else {
// Android lets users select all variants of languages we support in the system settings,
// so we need to find the closest match
// it should not happen that we find no match, but returning null is fine (picker will show default)
val availableLanguages = context.resources.getStringArray(R.array.language_values)
return availableLanguages.find { it == selectedLanguage[0]!!.toLanguageTag() }
?: availableLanguages.find { language ->
language.startsWith(selectedLanguage[0]!!.language)
}
}
} else {
prefs.getNonNullString(PrefKeys.LANGUAGE, DEFAULT)
}
}
private fun applyLanguageToApp(language: String?) {
val localeList = if (language == DEFAULT) {
LocaleListCompat.getEmptyLocaleList()
} else {
LocaleListCompat.forLanguageTags(language)
}
AppCompatDelegate.setApplicationLocales(localeList)
}
companion object {
private const val DEFAULT = "default"
private const val HANDLED_BY_SYSTEM = "handled_by_system"
}
}

View File

@ -0,0 +1,89 @@
/* Copyright 2022 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.util
import android.util.Log
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import com.keylesspalace.tusky.db.AccountEntity
import java.util.Locale
private const val TAG: String = "LocaleUtils"
private fun mergeLocaleListCompat(list: MutableList<Locale>, localeListCompat: LocaleListCompat) {
for (index in 0 until localeListCompat.size()) {
val locale = localeListCompat[index]
if (locale != null && list.none { locale.language == it.language }) {
list.add(locale)
}
}
}
// Ensure that the locale whose code matches the given language is first in the list
private fun ensureLanguageIsFirst(locales: MutableList<Locale>, language: String) {
var currentLocaleIndex = locales.indexOfFirst { it.language == language }
if (currentLocaleIndex < 0) {
// Recheck against modern language codes
// This should only happen when replying or when the per-account post language is set
// to a modern code
currentLocaleIndex = locales.indexOfFirst { it.modernLanguageCode == language }
if (currentLocaleIndex < 0) {
// This can happen when:
// - Your per-account posting language is set to one android doesn't know (e.g. toki pona)
// - Replying to a post in a language android doesn't know
locales.add(0, Locale(language))
Log.w(TAG, "Attempting to use unknown language tag '$language'")
return
}
}
if (currentLocaleIndex > 0) {
// Move preselected locale to the top
locales.add(0, locales.removeAt(currentLocaleIndex))
}
}
fun getInitialLanguage(language: String? = null, activeAccount: AccountEntity? = null): String {
return if (language.isNullOrEmpty()) {
// Account-specific language set on the server
if (activeAccount?.defaultPostLanguage?.isNotEmpty() == true) {
activeAccount.defaultPostLanguage
} else {
// Setting the application ui preference sets the default locale
AppCompatDelegate.getApplicationLocales()[0]?.language
?: Locale.getDefault().language
}
} else {
language
}
}
fun getLocaleList(initialLanguage: String): List<Locale> {
val locales = mutableListOf<Locale>()
mergeLocaleListCompat(locales, AppCompatDelegate.getApplicationLocales()) // configured app languages first
mergeLocaleListCompat(locales, LocaleListCompat.getDefault()) // then configured system languages
locales.addAll( // finally, other languages
// Only "base" languages, "en" but not "en_DK"
Locale.getAvailableLocales().filter {
it.country.isNullOrEmpty() &&
it.script.isNullOrEmpty() &&
it.variant.isNullOrEmpty()
}.sortedBy { it.displayName }
)
ensureLanguageIsFirst(locales, initialLanguage)
return locales
}

View File

@ -34,7 +34,10 @@ public class TimestampUtils {
public static String getRelativeTimeSpanString(Context context, long then, long now) {
long span = now - then;
boolean future = false;
if (span < 0) {
if (Math.abs(span) < SECOND_IN_MILLIS) {
return context.getString(R.string.status_created_at_now);
}
else if (span < 0) {
future = true;
span = -span;
}

View File

@ -47,6 +47,7 @@ fun Notification.toViewData(
this.type,
this.id,
this.account,
this.status?.toViewData(isShowingContent, isExpanded, isCollapsed)
this.status?.toViewData(isShowingContent, isExpanded, isCollapsed),
this.report,
)
}

View File

@ -0,0 +1,210 @@
package com.keylesspalace.tusky.view
import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import com.keylesspalace.tusky.R
import kotlin.math.roundToInt
/**
* Lays out a set of [MediaPreviewImageView]s keeping their aspect ratios into account.
*/
class MediaPreviewLayout(context: Context, attrs: AttributeSet? = null) :
ViewGroup(context, attrs) {
private val spacing = context.resources.getDimensionPixelOffset(R.dimen.preview_image_spacing)
/**
* An ordered list of aspect ratios used for layout. An image view for each aspect ratio passed
* will be attached. Supports up to 4, additional ones will be ignored.
*/
var aspectRatios: List<Double> = emptyList()
set(value) {
field = value
attachImageViews()
}
private val imageViewCache = Array(4) { MediaPreviewImageView(context) }
private var measuredOrientation = LinearLayout.VERTICAL
private fun attachImageViews() {
removeAllViews()
for (i in 0 until aspectRatios.size.coerceAtMost(imageViewCache.size)) {
addView(imageViewCache[i])
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val width = MeasureSpec.getSize(widthMeasureSpec)
val halfWidth = width / 2 - spacing / 2
var totalHeight = 0
when (childCount) {
1 -> {
val aspect = aspectRatios[0]
totalHeight += getChildAt(0).measureToAspect(width, aspect)
}
2 -> {
val aspect1 = aspectRatios[0]
val aspect2 = aspectRatios[1]
if ((aspect1 + aspect2) / 2 > 1.2) {
// stack vertically
measuredOrientation = LinearLayout.VERTICAL
totalHeight += getChildAt(0).measureToAspect(width, aspect1.coerceAtLeast(1.8))
totalHeight += spacing
totalHeight += getChildAt(1).measureToAspect(width, aspect2.coerceAtLeast(1.8))
} else {
// stack horizontally
measuredOrientation = LinearLayout.HORIZONTAL
val height = rowHeight(halfWidth, aspect1, aspect2)
totalHeight += height
getChildAt(0).measureExactly(halfWidth, height)
getChildAt(1).measureExactly(halfWidth, height)
}
}
3 -> {
val aspect1 = aspectRatios[0]
val aspect2 = aspectRatios[1]
val aspect3 = aspectRatios[2]
if (aspect1 >= 1) {
// | 1 |
// -------------
// | 2 | 3 |
measuredOrientation = LinearLayout.VERTICAL
totalHeight += getChildAt(0).measureToAspect(width, aspect1.coerceAtLeast(1.8))
totalHeight += spacing
val bottomHeight = rowHeight(halfWidth, aspect2, aspect3)
totalHeight += bottomHeight
getChildAt(1).measureExactly(halfWidth, bottomHeight)
getChildAt(2).measureExactly(halfWidth, bottomHeight)
} else {
// | | 2 |
// | 1 |-----|
// | | 3 |
measuredOrientation = LinearLayout.HORIZONTAL
val colHeight = getChildAt(0).measureToAspect(halfWidth, aspect1)
totalHeight += colHeight
val halfHeight = colHeight / 2 - spacing / 2
getChildAt(1).measureExactly(halfWidth, halfHeight)
getChildAt(2).measureExactly(halfWidth, halfHeight)
}
}
4 -> {
val aspect1 = aspectRatios[0]
val aspect2 = aspectRatios[1]
val aspect3 = aspectRatios[2]
val aspect4 = aspectRatios[3]
val topHeight = rowHeight(halfWidth, aspect1, aspect2)
totalHeight += topHeight
getChildAt(0).measureExactly(halfWidth, topHeight)
getChildAt(1).measureExactly(halfWidth, topHeight)
totalHeight += spacing
val bottomHeight = rowHeight(halfWidth, aspect3, aspect4)
totalHeight += bottomHeight
getChildAt(2).measureExactly(halfWidth, bottomHeight)
getChildAt(3).measureExactly(halfWidth, bottomHeight)
}
}
super.onMeasure(
widthMeasureSpec,
MeasureSpec.makeMeasureSpec(totalHeight, MeasureSpec.EXACTLY)
)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
val width = r - l
val height = b - t
val halfWidth = width / 2 - spacing / 2
when (childCount) {
1 -> {
getChildAt(0).layout(0, 0, width, height)
}
2 -> {
if (measuredOrientation == LinearLayout.VERTICAL) {
val y = imageViewCache[0].measuredHeight
getChildAt(0).layout(0, 0, width, y)
getChildAt(1).layout(
0,
y + spacing,
width,
y + spacing + getChildAt(1).measuredHeight
)
} else {
getChildAt(0).layout(0, 0, halfWidth, height)
getChildAt(1).layout(halfWidth + spacing, 0, width, height)
}
}
3 -> {
if (measuredOrientation == LinearLayout.VERTICAL) {
val y = getChildAt(0).measuredHeight
getChildAt(0).layout(0, 0, width, y)
getChildAt(1).layout(0, y + spacing, halfWidth, height)
getChildAt(2).layout(halfWidth + spacing, y + spacing, width, height)
} else {
val colHeight = getChildAt(0).measuredHeight
getChildAt(0).layout(0, 0, halfWidth, colHeight)
val halfHeight = colHeight / 2 - spacing / 2
getChildAt(1).layout(halfWidth + spacing, 0, width, halfHeight)
getChildAt(2).layout(
halfWidth + spacing,
halfHeight + spacing,
width,
colHeight
)
}
}
4 -> {
val topHeight = (getChildAt(0).measuredHeight + getChildAt(1).measuredHeight) / 2
getChildAt(0).layout(0, 0, halfWidth, topHeight)
getChildAt(1).layout(halfWidth + spacing, 0, width, topHeight)
val bottomHeight =
(imageViewCache[2].measuredHeight + imageViewCache[3].measuredHeight) / 2
getChildAt(2).layout(
0,
topHeight + spacing,
halfWidth,
topHeight + spacing + bottomHeight
)
getChildAt(3).layout(
halfWidth + spacing,
topHeight + spacing,
width,
topHeight + spacing + bottomHeight
)
}
}
}
inline fun forEachIndexed(action: (Int, MediaPreviewImageView) -> Unit) {
for (index in 0 until childCount) {
action(index, getChildAt(index) as MediaPreviewImageView)
}
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
}
}
private fun rowHeight(halfWidth: Int, aspect1: Double, aspect2: Double): Int {
return ((halfWidth / aspect1 + halfWidth / aspect2) / 2).roundToInt()
}
private fun View.measureToAspect(width: Int, aspect: Double): Int {
val height = (width / aspect).roundToInt()
measureExactly(width, height)
return height
}
private fun View.measureExactly(width: Int, height: Int) {
measure(
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
)
}

View File

@ -17,8 +17,8 @@ package com.keylesspalace.tusky.viewdata;
import androidx.annotation.Nullable;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.entity.Report;
import com.keylesspalace.tusky.entity.TimelineAccount;
import java.util.Objects;
@ -48,13 +48,16 @@ public abstract class NotificationViewData {
private final TimelineAccount account;
@Nullable
private final StatusViewData.Concrete statusViewData;
@Nullable
private final Report report;
public Concrete(Notification.Type type, String id, TimelineAccount account,
@Nullable StatusViewData.Concrete statusViewData) {
@Nullable StatusViewData.Concrete statusViewData, @Nullable Report report) {
this.type = type;
this.id = id;
this.account = account;
this.statusViewData = statusViewData;
this.report = report;
}
public Notification.Type getType() {
@ -74,6 +77,11 @@ public abstract class NotificationViewData {
return statusViewData;
}
@Nullable
public Report getReport() {
return report;
}
@Override
public long getViewDataId() {
return id.hashCode();
@ -87,7 +95,8 @@ public abstract class NotificationViewData {
return type == concrete.type &&
Objects.equals(id, concrete.id) &&
account.getId().equals(concrete.account.getId()) &&
(Objects.equals(statusViewData, concrete.statusViewData));
(Objects.equals(statusViewData, concrete.statusViewData)) &&
(Objects.equals(report, concrete.report));
}
@Override
@ -97,7 +106,7 @@ public abstract class NotificationViewData {
}
public Concrete copyWithStatus(@Nullable StatusViewData.Concrete statusViewData) {
return new Concrete(type, id, account, statusViewData);
return new Concrete(type, id, account, statusViewData, report);
}
}

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="@color/tusky_blue">
<path
android:fillColor="@android:color/white"
android:pathData="M14.4,6L14,4H5v17h2v-7h5.6l0.4,2h7V6z"/>
</vector>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@drawable/ic_play_indicator"
android:gravity="center" />
</layer-list>

View File

@ -14,10 +14,10 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="@string/title_view_thread"
app:navigationIcon="?attr/homeAsUpIndicator"
app:menu="@menu/view_thread_toolbar"
app:navigationContentDescription="@string/abc_action_bar_up_description"
app:menu="@menu/view_thread_toolbar" />
app:navigationIcon="?attr/homeAsUpIndicator"
app:title="@string/title_view_thread" />
</com.google.android.material.appbar.AppBarLayout>
@ -25,8 +25,8 @@
android:id="@+id/swipeRefreshLayout"
android:layout_width="640dp"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
android:layout_gravity="center_horizontal">
android:layout_gravity="center_horizontal|top"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
@ -47,7 +47,7 @@
android:id="@+id/statusView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_gravity="center" />
android:layout_gravity="center"
android:visibility="gone" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.keylesspalace.tusky.components.followedtags.FollowedTagsActivity">
<include
android:id="@+id/includedToolbar"
layout="@layout/toolbar_basic" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/followedTagsView"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:itemCount="5"
tools:listitem="@layout/item_followed_hashtag"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
/>
<com.keylesspalace.tusky.view.BackgroundMessageView
android:id="@+id/followedTagsMessageView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
/>
<ProgressBar
android:id="@+id/followedTagsProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -28,20 +28,38 @@
<androidx.appcompat.widget.Toolbar
android:id="@+id/mainToolbar"
style="@style/Widget.AppCompat.Toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:contentInsetStartWithNavigation="0dp"
app:layout_scrollFlags="scroll|enterAlways"
app:navigationContentDescription="@string/action_open_drawer" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
style="@style/TuskyTabAppearance"
<LinearLayout
android:id="@+id/topNav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabGravity="fill"
app:tabMaxWidth="0dp"
app:tabMode="fixed" />
android:orientation="horizontal">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/topNavAvatar"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:background="@drawable/avatar_default"
app:shapeAppearance="@style/ShapeAppearance.Avatar" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
style="@style/TuskyTabAppearance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabGravity="fill"
app:tabMaxWidth="0dp"
app:tabMode="fixed" />
</LinearLayout>
</com.google.android.material.appbar.AppBarLayout>
@ -61,13 +79,30 @@
app:contentInsetStart="0dp"
app:fabAlignmentMode="end">
<com.google.android.material.tabs.TabLayout
android:id="@+id/bottomTabLayout"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:tabIndicator="@null"
app:tabGravity="fill"
app:tabMode="fixed" />
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/bottomNavAvatar"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:background="@drawable/avatar_default"
app:shapeAppearance="@style/ShapeAppearance.Avatar" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/bottomTabLayout"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:tabGravity="fill"
app:tabIndicator="@null"
app:tabMode="fixed" />
</LinearLayout>
</com.google.android.material.bottomappbar.BottomAppBar>
@ -87,8 +122,8 @@
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_gravity="center" />
android:layout_gravity="center"
android:visibility="gone" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -29,6 +29,7 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:contentDescription="@string/action_add_tab"
android:src="@drawable/ic_plus_24dp" />
<com.google.android.material.card.MaterialCardView

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/listsView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
tools:visibility="visible" />
<com.keylesspalace.tusky.view.BackgroundMessageView
android:id="@+id/messageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@android:color/transparent"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/elephant_error"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>
<Button
android:id="@+id/doneButton"
style="@style/TuskyButton.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/button_done" />
</LinearLayout>

View File

@ -14,10 +14,10 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="@string/title_view_thread"
app:navigationIcon="?attr/homeAsUpIndicator"
app:menu="@menu/view_thread_toolbar"
app:navigationContentDescription="@string/abc_action_bar_up_description"
app:menu="@menu/view_thread_toolbar" />
app:navigationIcon="?attr/homeAsUpIndicator"
app:title="@string/title_view_thread" />
</com.google.android.material.appbar.AppBarLayout>
@ -25,8 +25,8 @@
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
android:layout_gravity="top">
android:layout_gravity="top"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
@ -47,7 +47,7 @@
android:id="@+id/statusView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_gravity="center" />
android:layout_gravity="center"
android:visibility="gone" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp">
<TextView
android:id="@+id/listNameView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:drawablePadding="8dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:textColor="?android:attr/textColorSecondary"
android:textSize="?attr/status_text_medium"
app:drawableStartCompat="@drawable/ic_list"
app:drawableTint="?android:attr/textColorSecondary"
tools:text="Example list" />
<ImageButton
android:id="@+id/addButton"
style="@style/TuskyImageButton"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="12dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_add_to_list"
android:padding="4dp"
android:src="@drawable/ic_plus_24dp" />
<ImageButton
android:id="@+id/removeButton"
style="@style/TuskyImageButton"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="12dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_remove_from_list"
android:padding="4dp"
android:src="@drawable/ic_clear_24dp"
android:visibility="gone" />
</LinearLayout>

View File

@ -20,8 +20,8 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text">
<com.google.android.material.chip.Chip
@ -30,6 +30,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checkable="false"
android:contentDescription="@string/action_add_reaction"
app:chipEndPadding="4dp"
app:chipIcon="@drawable/ic_plus_24dp"
app:chipSurfaceColor="@color/tusky_blue"

View File

@ -4,8 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingStart="16dp"
android:paddingBottom="10dp">
<TextView
@ -13,7 +12,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:drawableStart="@drawable/ic_person_add_24dp"
android:drawablePadding="10dp"
android:ellipsize="end"
android:gravity="center_vertical"
@ -21,6 +19,7 @@
android:paddingStart="28dp"
android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium"
app:drawableStartCompat="@drawable/ic_person_add_24dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Someone requested to follow you" />
@ -32,13 +31,13 @@
android:layout_centerVertical="true"
android:layout_marginTop="10dp"
android:contentDescription="@string/action_view_profile"
tools:src="@drawable/avatar_default"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/notificationTextView" />
app:layout_constraintTop_toBottomOf="@id/notificationTextView"
tools:src="@drawable/avatar_default" />
<TextView
android:id="@+id/displayNameTextView"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:ellipsize="end"
@ -46,54 +45,56 @@
android:textColor="?android:textColorPrimary"
android:textSize="?attr/status_text_large"
android:textStyle="normal|bold"
app:layout_constraintBottom_toTopOf="@id/usernameTextView"
app:layout_constraintEnd_toStartOf="@id/rejectButton"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toTopOf="@id/avatar"
app:layout_constraintBottom_toTopOf="@id/usernameTextView"
tools:text="Display name" />
<TextView
android:id="@+id/usernameTextView"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium"
app:layout_constraintBottom_toBottomOf="@id/avatar"
app:layout_constraintEnd_toStartOf="@id/rejectButton"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toBottomOf="@id/displayNameTextView"
app:layout_constraintBottom_toBottomOf="@id/avatar"
tools:text="\@username" />
<ImageButton
android:id="@+id/rejectButton"
style="@style/TuskyImageButton"
android:layout_width="52dp"
android:layout_height="48dp"
android:layout_centerVertical="true"
android:layout_marginStart="12dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_reject"
android:padding="4dp"
app:layout_constraintBottom_toBottomOf="@id/avatar"
app:layout_constraintEnd_toStartOf="@id/acceptButton"
app:layout_constraintStart_toEndOf="@id/displayNameTextView"
app:layout_constraintTop_toTopOf="@id/avatar"
app:srcCompat="@drawable/ic_reject_24dp" />
<ImageButton
android:id="@+id/acceptButton"
style="@style/TuskyImageButton"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_width="52dp"
android:layout_height="48dp"
android:layout_centerVertical="true"
android:layout_marginStart="12dp"
android:layout_marginEnd="4dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_accept"
android:padding="4dp"
app:layout_constraintEnd_toStartOf="@id/rejectButton"
app:layout_constraintTop_toTopOf="@id/avatar"
app:layout_constraintBottom_toBottomOf="@id/avatar"
app:srcCompat="@drawable/ic_check_24dp" />
<ImageButton
android:id="@+id/rejectButton"
style="@style/TuskyImageButton"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_centerVertical="true"
android:layout_marginStart="12dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_reject"
android:padding="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/avatar"
app:layout_constraintBottom_toBottomOf="@id/avatar"
app:srcCompat="@drawable/ic_reject_24dp" />
app:srcCompat="@drawable/ic_check_24dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="72dp"
android:gravity="center_vertical"
android:paddingLeft="16dp"
android:paddingRight="16dp"
>
<ImageButton
android:id="@+id/followed_tag_unfollow"
style="@style/TuskyImageButton"
android:layout_width="32dp"
android:layout_height="32dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_unfollow"
android:padding="4dp"
app:srcCompat="@drawable/ic_person_remove_24dp"
/>
<TextView
android:id="@+id/followed_tag"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:gravity="center_vertical"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium"
tools:text="#hashtag" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,198 +1,113 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<com.keylesspalace.tusky.view.MediaPreviewImageView
android:id="@+id/status_media_preview_0"
android:layout_width="0dp"
android:layout_height="@dimen/status_media_preview_height"
android:scaleType="centerCrop"
app:layout_constraintEnd_toStartOf="@+id/status_media_preview_1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<com.keylesspalace.tusky.view.MediaPreviewLayout
android:id="@+id/status_media_preview"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toStartOf="@+id/status_media_preview_1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.keylesspalace.tusky.view.MediaPreviewImageView
android:id="@+id/status_media_preview_1"
android:layout_width="0dp"
android:layout_height="@dimen/status_media_preview_height"
android:layout_marginStart="4dp"
android:scaleType="centerCrop"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/status_media_preview_0"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/status_sensitive_media_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0.7"
android:contentDescription="@null"
android:padding="@dimen/status_sensitive_media_button_padding"
android:visibility="gone"
app:layout_constraintLeft_toLeftOf="@+id/status_media_preview_container"
app:layout_constraintTop_toTopOf="@+id/status_media_preview_container"
app:srcCompat="@drawable/ic_eye_24dp"
app:tint="@color/white" />
<com.keylesspalace.tusky.view.MediaPreviewImageView
android:id="@+id/status_media_preview_2"
android:layout_width="0dp"
android:layout_height="@dimen/status_media_preview_height"
android:layout_marginTop="4dp"
android:scaleType="centerCrop"
app:layout_constraintEnd_toStartOf="@+id/status_media_preview_3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/status_media_preview_0"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/status_sensitive_media_warning"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/media_warning_bg"
android:gravity="center"
android:lineSpacingMultiplier="1.2"
android:orientation="vertical"
android:paddingLeft="12dp"
android:paddingTop="8dp"
android:paddingRight="12dp"
android:paddingBottom="8dp"
android:textAlignment="center"
android:textColor="?android:attr/textColorSecondary"
android:textSize="?attr/status_text_medium"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.keylesspalace.tusky.view.MediaPreviewImageView
android:id="@+id/status_media_preview_3"
android:layout_width="0dp"
android:layout_height="@dimen/status_media_preview_height"
android:layout_marginStart="4dp"
android:layout_marginTop="4dp"
android:background="@drawable/media_preview_outline"
android:scaleType="centerCrop"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/status_media_preview_2"
app:layout_constraintTop_toBottomOf="@+id/status_media_preview_1"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/status_media_label_0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:drawablePadding="4dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:importantForAccessibility="no"
android:maxLines="10"
android:textSize="?attr/status_text_medium"
android:visibility="gone"
app:drawableTint="?android:attr/textColorTertiary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/status_media_overlay_0"
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="center"
app:layout_constraintBottom_toBottomOf="@+id/status_media_preview_0"
app:layout_constraintEnd_toEndOf="@+id/status_media_preview_0"
app:layout_constraintStart_toStartOf="@+id/status_media_preview_0"
app:layout_constraintTop_toTopOf="@+id/status_media_preview_0"
app:srcCompat="@drawable/ic_play_indicator"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/status_media_label_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:drawablePadding="4dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:importantForAccessibility="no"
android:maxLines="10"
android:textSize="?attr/status_text_medium"
android:visibility="gone"
app:drawableTint="?android:attr/textColorTertiary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/status_media_label_0" />
<ImageView
android:id="@+id/status_media_overlay_1"
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="center"
app:layout_constraintBottom_toBottomOf="@+id/status_media_preview_1"
app:layout_constraintEnd_toEndOf="@+id/status_media_preview_1"
app:layout_constraintStart_toStartOf="@+id/status_media_preview_1"
app:layout_constraintTop_toTopOf="@+id/status_media_preview_1"
app:srcCompat="@drawable/ic_play_indicator"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/status_media_label_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:drawablePadding="4dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:importantForAccessibility="no"
android:maxLines="10"
android:textSize="?attr/status_text_medium"
android:visibility="gone"
app:drawableTint="?android:attr/textColorTertiary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/status_media_label_1" />
<ImageView
android:id="@+id/status_media_overlay_2"
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="center"
app:layout_constraintBottom_toBottomOf="@+id/status_media_preview_2"
app:layout_constraintEnd_toEndOf="@+id/status_media_preview_2"
app:layout_constraintStart_toStartOf="@+id/status_media_preview_2"
app:layout_constraintTop_toTopOf="@+id/status_media_preview_2"
app:srcCompat="@drawable/ic_play_indicator"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/status_media_overlay_3"
android:layout_width="0dp"
android:layout_height="0dp"
android:scaleType="center"
app:layout_constraintBottom_toBottomOf="@+id/status_media_preview_3"
app:layout_constraintEnd_toEndOf="@+id/status_media_preview_3"
app:layout_constraintStart_toStartOf="@+id/status_media_preview_3"
app:layout_constraintTop_toTopOf="@+id/status_media_preview_3"
app:srcCompat="@drawable/ic_play_indicator"
tools:ignore="ContentDescription" />
<ImageView
android:id="@+id/status_sensitive_media_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0.7"
android:contentDescription="@null"
android:padding="@dimen/status_sensitive_media_button_padding"
android:visibility="gone"
app:layout_constraintLeft_toLeftOf="@+id/status_media_preview_container"
app:layout_constraintTop_toTopOf="@+id/status_media_preview_container"
app:srcCompat="@drawable/ic_eye_24dp"
app:tint="@color/white" />
<TextView
android:id="@+id/status_sensitive_media_warning"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/media_warning_bg"
android:gravity="center"
android:lineSpacingMultiplier="1.2"
android:orientation="vertical"
android:paddingLeft="12dp"
android:paddingTop="8dp"
android:paddingRight="12dp"
android:paddingBottom="8dp"
android:textAlignment="center"
android:textColor="?android:attr/textColorSecondary"
android:textSize="?attr/status_text_medium"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/status_media_label_0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:drawablePadding="4dp"
android:gravity="center_vertical"
android:importantForAccessibility="no"
android:textSize="?attr/status_text_medium"
android:visibility="gone"
android:maxLines="10"
android:ellipsize="end"
app:drawableTint="?android:attr/textColorTertiary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/status_media_label_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:drawablePadding="4dp"
android:gravity="center_vertical"
android:importantForAccessibility="no"
android:textSize="?attr/status_text_medium"
android:visibility="gone"
android:maxLines="10"
android:ellipsize="end"
app:drawableTint="?android:attr/textColorTertiary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/status_media_label_0" />
<TextView
android:id="@+id/status_media_label_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:drawablePadding="4dp"
android:gravity="center_vertical"
android:importantForAccessibility="no"
android:textSize="?attr/status_text_medium"
android:visibility="gone"
android:maxLines="10"
android:ellipsize="end"
app:drawableTint="?android:attr/textColorTertiary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/status_media_label_1" />
<TextView
android:id="@+id/status_media_label_3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:drawablePadding="4dp"
android:gravity="center_vertical"
android:importantForAccessibility="no"
android:textSize="?attr/status_text_medium"
android:visibility="gone"
android:maxLines="10"
android:ellipsize="end"
app:drawableTint="?android:attr/textColorTertiary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/status_media_label_2" />
<TextView
android:id="@+id/status_media_label_3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:drawablePadding="4dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:importantForAccessibility="no"
android:maxLines="10"
android:textSize="?attr/status_text_medium"
android:visibility="gone"
app:drawableTint="?android:attr/textColorTertiary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/status_media_label_2" />
</merge>

View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/notification_report"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="14dp"
android:paddingRight="14dp">
<TextView
android:id="@+id/notification_top_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginTop="8dp"
android:drawablePadding="10dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:paddingStart="28dp"
android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium"
tools:text="Someone reported someone else" />
<ImageView
android:id="@+id/notification_reportee_avatar"
android:layout_width="48dp"
android:layout_height="48dp"
app:layout_constraintTop_toBottomOf="@id/notification_top_text"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginTop="10dp"
android:layout_marginEnd="14dp"
android:layout_marginBottom="14dp"
android:contentDescription="@string/action_view_profile"
android:scaleType="centerCrop"
tools:ignore="RtlHardcoded,RtlSymmetry"
tools:src="@drawable/avatar_default" />
<ImageView
android:id="@+id/notification_reporter_avatar"
android:layout_width="24dp"
android:layout_height="24dp"
app:layout_constraintRight_toRightOf="@id/notification_reportee_avatar"
app:layout_constraintBottom_toBottomOf="@id/notification_reportee_avatar"
/>
<TextView
android:id="@+id/notification_summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="-4dp"
android:layout_marginStart="14dp"
app:layout_constraintTop_toTopOf="@id/notification_reportee_avatar"
app:layout_constraintLeft_toRightOf="@id/notification_reporter_avatar"
android:importantForAccessibility="no"
android:hyphenationFrequency="full"
android:lineSpacingMultiplier="1.1"
android:textColor="?android:textColorTertiary"
android:textSize="?attr/status_text_medium"
tools:text="30 minutes ago - 2 posts" />
<TextView
android:id="@+id/notification_category"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
app:layout_constraintTop_toBottomOf="@id/notification_summary"
app:layout_constraintLeft_toRightOf="@id/notification_reporter_avatar"
android:importantForAccessibility="no"
android:hyphenationFrequency="full"
android:lineSpacingMultiplier="1.1"
android:paddingBottom="10dp"
android:textColor="?android:textColorTertiary"
android:textSize="?attr/status_text_medium"
android:textStyle="bold"
tools:text="Spam" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -18,6 +18,10 @@
android:title="@string/action_block"
app:showAsAction="never" />
<item android:id="@+id/action_add_or_remove_from_list"
android:title="@string/action_add_or_remove_from_list"
app:showAsAction="never" />
<item android:id="@+id/action_mute_domain"
android:title="@string/action_mute_domain"
app:showAsAction="never" />
@ -29,4 +33,4 @@
<item
android:id="@+id/action_report"
android:title="@string/action_report" />
</menu>
</menu>

View File

@ -17,5 +17,18 @@
app:iconTint="?attr/colorOnSurface"
android:icon="@drawable/ic_person_remove_24dp" />
<item
android:id="@+id/action_mute_hashtag"
android:title="Mute"
app:showAsAction="ifRoom"
app:iconTint="?attr/colorOnSurface"
android:icon="@drawable/ic_mute_24dp" />
<item
android:id="@+id/action_unmute_hashtag"
android:title="Unmute"
app:showAsAction="ifRoom"
app:iconTint="?attr/colorOnSurface"
android:icon="@drawable/ic_unmute_24dp" />
</menu>

View File

@ -2,29 +2,29 @@
<resources>
<string name="error_generic">Vyskytla se chyba.</string>
<string name="error_network">Vyskytla se chyba sítě! Prosím zkontrolujte své připojení a zkuste to znovu!</string>
<string name="error_empty">Tohle nemůže být prázdné.</string>
<string name="error_invalid_domain">Neplatná doména zadána</string>
<string name="error_failed_app_registration">Autentizace s tímto serverem neuspěla.</string>
<string name="error_no_web_browser_found">Nelze najít webový prohlížeč k použití.</string>
<string name="error_empty">Toto nemůže být prázdné.</string>
<string name="error_invalid_domain">Byla zadána neplatná doména</string>
<string name="error_failed_app_registration">Autentizace s tímto serverem nebyla úspěšná.</string>
<string name="error_no_web_browser_found">Nepodařilo se najít webový prohlížeč, který lze použít.</string>
<string name="error_authorization_unknown">Vyskytla se neidentifikovaná chyba autorizace.</string>
<string name="error_authorization_denied">Autorizace byla zamítnuta.</string>
<string name="error_retrieving_oauth_token">Nepodařilo se získat přihlašovací token.</string>
<string name="error_compose_character_limit">Toot je příliš dlouhý!</string>
<string name="error_compose_character_limit">Příspěvek je příliš dlouhý!</string>
<string name="error_media_upload_type">Tento typ souboru nemůže být nahrán.</string>
<string name="error_media_upload_opening">Tento soubor nemohl být otevřen.</string>
<string name="error_media_upload_permission">Je vyžadováno povolení číst média.</string>
<string name="error_media_download_permission">Je vyžadováno povolení ukládat média.</string>
<string name="error_media_upload_image_or_video">K jednomu tootu nemohou být přiloženy obrázky i videa.</string>
<string name="error_media_upload_sending">Nahrání selhalo.</string>
<string name="error_sender_account_gone">Chyba při odesílání tootu.</string>
<string name="error_media_upload_opening">Tento soubor se nepodařilo otevřít.</string>
<string name="error_media_upload_permission">Je vyžadováno oprávnění ke čtení médií.</string>
<string name="error_media_download_permission">Je vyžadováno oprávnění ukládat média.</string>
<string name="error_media_upload_image_or_video">K jednomu příspěvku nemohou být přiloženy obrázky i videa.</string>
<string name="error_media_upload_sending">Nahrání se nezdařilo.</string>
<string name="error_sender_account_gone">Chyba při odesílání příspěvku.</string>
<string name="title_home">Domů</string>
<string name="title_notifications">Oznámení</string>
<string name="title_public_local">Místní</string>
<string name="title_public_federated">Federovaná</string>
<string name="title_public_federated">Federované</string>
<string name="title_direct_messages">Přímé zprávy</string>
<string name="title_tab_preferences">Panely</string>
<string name="title_view_thread">Toot</string>
<string name="title_posts">Tooty</string>
<string name="title_view_thread">Vlákno</string>
<string name="title_posts">Příspěvky</string>
<string name="title_posts_with_replies">S odpověďmi</string>
<string name="title_posts_pinned">Připnuté</string>
<string name="title_follows">Sledovaní</string>
@ -44,31 +44,31 @@
<string name="post_content_warning_show_more">Zobrazit více</string>
<string name="post_content_warning_show_less">Zobrazit méně</string>
<string name="post_content_show_more">Rozbalit</string>
<string name="post_content_show_less">Zabalit</string>
<string name="post_content_show_less">Sbalit</string>
<string name="message_empty">Tady nic není.</string>
<string name="footer_empty">Tady nic není. Obnovte přetažnením dolů!</string>
<string name="notification_reblog_format">%s boostnul/a váš toot</string>
<string name="notification_favourite_format">%s si oblíbil/a váš toot</string>
<string name="footer_empty">Tady nic není. Obnovte přetažením dolů!</string>
<string name="notification_reblog_format">%s boostnul/a váš příspěvek</string>
<string name="notification_favourite_format">%s si oblíbil/a váš příspěvek</string>
<string name="notification_follow_format">%s vás nyní sleduje</string>
<string name="report_username_format">Nahlásit uživatele @%s</string>
<string name="report_comment_hint">Dodatečné komentáře?</string>
<string name="report_comment_hint">Další komentáře\?</string>
<string name="action_quick_reply">Rychlá odpověď</string>
<string name="action_reply">Odpovědět</string>
<string name="action_reblog">Boostnout</string>
<string name="action_unreblog">Odstranit boost</string>
<string name="action_favourite">Oblíbit</string>
<string name="action_unfavourite">Odstranit oblíbení</string>
<string name="action_more">Další</string>
<string name="action_more">Více</string>
<string name="action_compose">Napsat</string>
<string name="action_login">Přihlásit účtem Mastodon</string>
<string name="action_logout">Odhlásit</string>
<string name="action_login">Přihlásit se účtem Mastodon</string>
<string name="action_logout">Odhlásit se</string>
<string name="action_logout_confirm">Jste si jistý/á, že se chcete odhlásit z účtu %1$s?</string>
<string name="action_follow">Sledovat</string>
<string name="action_unfollow">Přestat sledovat</string>
<string name="action_block">Blokovat</string>
<string name="action_unblock">Odblokovat</string>
<string name="action_hide_reblogs">Skrýt boosty</string>
<string name="action_show_reblogs">Zobrazi boosty</string>
<string name="action_show_reblogs">Zobrazit boosty</string>
<string name="action_report">Nahlásit</string>
<string name="action_delete">Smazat</string>
<string name="action_send">TOOTNOUT</string>
@ -85,10 +85,10 @@
<string name="action_view_media">Média</string>
<string name="action_open_in_web">Otevřít v prohlížeči</string>
<string name="action_add_media">Přidat média</string>
<string name="action_photo_take">Požídit fotku</string>
<string name="action_photo_take">Pořídit fotku</string>
<string name="action_share">Sdílet</string>
<string name="action_mute">Skrýt</string>
<string name="action_unmute">Odkrýt</string>
<string name="action_unmute">Zrušit skrytí</string>
<string name="action_mention">Zmínit</string>
<string name="action_hide_media">Skrýt média</string>
<string name="action_open_drawer">Otevřít menu</string>
@ -100,7 +100,7 @@
<string name="action_reject">Zamítnout</string>
<string name="action_search">Hledat</string>
<string name="action_access_drafts">Koncepty</string>
<string name="action_toggle_visibility">Viditelnost tootu</string>
<string name="action_toggle_visibility">Viditelnost příspěvku</string>
<string name="action_content_warning">Varování o obsahu</string>
<string name="action_emoji_keyboard">Klávesnice s emoji</string>
<string name="action_add_tab">Přidat panel</string>
@ -109,7 +109,7 @@
<string name="action_hashtags">Hashtagy</string>
<string name="action_open_reblogger">Otevřít autora boostu</string>
<string name="action_open_reblogged_by">Zobrazit boosty</string>
<string name="action_open_faved_by">Zobrazit oblíbené</string>
<string name="action_open_faved_by">Zobrazit oblíbení</string>
<string name="title_hashtags_dialog">Hashtagy</string>
<string name="title_mentions_dialog">Zmínky</string>
<string name="title_links_dialog">Odkazy</string>
@ -120,12 +120,12 @@
<string name="action_share_as">Sdílet jako…</string>
<string name="download_media">Stáhnout média</string>
<string name="downloading_media">Stahuji média</string>
<string name="send_post_link_to">Sdílet URL tootu na…</string>
<string name="send_post_content_to">Sdílet toot na…</string>
<string name="send_post_link_to">Sdílet URL příspěvku na…</string>
<string name="send_post_content_to">Sdílet příspěvek na…</string>
<string name="send_media_to">Sdílet média na…</string>
<string name="confirmation_reported">Odesláno!</string>
<string name="confirmation_unblocked">Uživatel odblokován</string>
<string name="confirmation_unmuted">Uživatel odkryt</string>
<string name="confirmation_unblocked">Uživatel byl odblokován</string>
<string name="confirmation_unmuted">Skrytí uživatele bylo zrušeno</string>
<string name="post_sent">Odesláno!</string>
<string name="post_sent_long">Odpověď byla úspěšně odeslána.</string>
<string name="hint_domain">Který server?</string>
@ -138,7 +138,7 @@
<string name="label_quick_reply">Odpovědět…</string>
<string name="label_avatar">Avatar</string>
<string name="label_header">Záhlaví</string>
<string name="link_whats_an_instance">Co je server?</string>
<string name="link_whats_an_instance">Co je to server\?</string>
<string name="login_connection">Připojuji se…</string>
<string name="dialog_whats_an_instance">Sem může být zadána adresa či doména jakéhokoliv
serveru, například mastodon.social, icosahedron.website, social.tchncs.de
@ -154,11 +154,11 @@
<string name="dialog_download_image">Stáhnout</string>
<string name="dialog_message_cancel_follow_request">Zrušit požadavek o sledování?</string>
<string name="dialog_unfollow_warning">Přestat sledovat tento účet?</string>
<string name="dialog_delete_post_warning">Smazat tento toot?</string>
<string name="dialog_delete_post_warning">Smazat tento příspěvek\?</string>
<string name="visibility_public">Veřejný: Poslat na veřejné časové osy</string>
<string name="visibility_unlisted">Neuvedený: Neposlat na veřejné časové osy</string>
<string name="visibility_private">Pouze pro sledující: Poslat pouze sledujícím</string>
<string name="visibility_direct">Přímý: Poslat pouze zmíněným uživatelům</string>
<string name="visibility_direct">Přímé: Poslat pouze zmíněným uživatelům</string>
<string name="pref_title_edit_notification_settings">Oznámení</string>
<string name="pref_title_notifications_enabled">Oznámení</string>
<string name="pref_title_notification_alerts">Upozornění</string>
@ -177,7 +177,7 @@
<string name="app_them_dark">Tmavý</string>
<string name="app_theme_light">Světlý</string>
<string name="app_theme_black">Černý</string>
<string name="app_theme_auto">Automatický při západu slunce</string>
<string name="app_theme_auto">Automaticky při západu slunce</string>
<string name="app_theme_system">Použít systémový design</string>
<string name="pref_title_browser_settings">Prohlížeč</string>
<string name="pref_title_custom_tabs">Používat Vlastní karty Chrome</string>
@ -185,7 +185,7 @@
<string name="pref_title_language">Jazyk</string>
<string name="pref_title_post_filter">Filtrování časových os</string>
<string name="pref_title_post_tabs">Panely</string>
<string name="pref_title_show_boosts">Zobrazi boosty</string>
<string name="pref_title_show_boosts">Zobrazit boosty</string>
<string name="pref_title_show_replies">Zobrazit odpovědi</string>
<string name="pref_title_show_media_preview">Stahovat náhledy médií</string>
<string name="pref_title_proxy_settings">Proxy</string>
@ -211,14 +211,16 @@
<string name="notification_follow_name">Noví sledující</string>
<string name="notification_follow_description">Oznámení o nových sledujících</string>
<string name="notification_boost_name">Boosty</string>
<string name="notification_boost_description">Oznámení, když jsou vaše tooty boostnuty</string>
<string name="notification_boost_description">Oznámení, když jsou vaše příspěvky boostnuty</string>
<string name="notification_favourite_name">Oblíbení</string>
<string name="notification_favourite_description">Oznámení, když jsou vaše tooty označeny jako oblíbené</string>
<string name="notification_favourite_description">Oznámení, když jsou vaše příspěvky označeny jako oblíbené</string>
<string name="notification_mention_format">%s vás zmínil/a</string>
<string name="notification_summary_large">%1$s, %2$s, %3$s a dalších %4$d</string>
<string name="notification_summary_medium">%1$s, %2$s a %3$s</string>
<string name="notification_summary_small">%1$s a %2$s</string>
<plurals name="notification_title_summary">
<item quantity="one">%d nová interakce</item>
<item quantity="few">%d nové interakce</item>
<item quantity="other">%d nových interakcí</item>
</plurals>
<string name="description_account_locked">Uzamčený účet</string>
@ -240,8 +242,8 @@
https://github.com/accelforce/Yuito/issues
</string>
<string name="about_tusky_account">Profil aplikace Yuito</string>
<string name="post_share_content">Sdílet obsah tootu</string>
<string name="post_share_link">Sdílet odkaz k tootu</string>
<string name="post_share_content">Sdílet obsah příspěvku</string>
<string name="post_share_link">Sdílet odkaz na příspěvek</string>
<string name="post_media_images">Obrázky</string>
<string name="post_media_video">Video</string>
<string name="state_follow_requested">Vyžádáno sledování</string>
@ -249,7 +251,7 @@
<string name="abbreviated_in_years">za %d let</string>
<string name="abbreviated_in_days">za %d d</string>
<string name="abbreviated_in_hours">za %d h</string>
<string name="abbreviated_in_minutes">za %d min</string>
<string name="abbreviated_in_minutes">za %d m</string>
<string name="abbreviated_in_seconds">za %d s</string>
<string name="abbreviated_years_ago">%d let</string>
<string name="abbreviated_days_ago">%d d</string>
@ -282,30 +284,35 @@
<string name="hint_search_people_list">Hledejte mezi lidmi, které sledujete</string>
<string name="action_add_to_list">Přidat účet na seznam</string>
<string name="action_remove_from_list">Odstranit účet ze seznamu</string>
<string name="compose_active_account_description">Píšete s účtem %1$s</string>
<string name="compose_active_account_description">Píšete jako %1$s</string>
<string name="error_failed_set_caption">Nastavení popisku selhalo</string>
<plurals name="hint_describe_for_visually_impaired">
<item quantity="other">Popis pro zrakově postižené\n(limit %d znaků)</item>
<item quantity="one">Popis pro zrakově postižené
\n(limit %d znak)</item>
<item quantity="few">Popis pro zrakově postižené
\n(limit %d znaky)</item>
<item quantity="other">Popis pro zrakově postižené
\n(limit %d znaků)</item>
</plurals>
<string name="action_set_caption">Nastavit popisek</string>
<string name="action_remove">Odstranit</string>
<string name="lock_account_label">Uzamknout účet</string>
<string name="lock_account_label_description">Vyžaduje, abyste ručně schvaloval/a sledující</string>
<string name="compose_save_draft">Uložit koncept?</string>
<string name="send_post_notification_title">Odesílám toot</string>
<string name="send_post_notification_error_title">Chyba při odesílání tootu</string>
<string name="send_post_notification_channel_name">Odesílám tooty</string>
<string name="send_post_notification_title">Odesílám příspěvek</string>
<string name="send_post_notification_error_title">Chyba při odesílání příspěvku</string>
<string name="send_post_notification_channel_name">Odesílám příspěvky</string>
<string name="send_post_notification_cancel_title">Odesílání bylo zrušeno</string>
<string name="send_post_notification_saved_content">Kopie vašeho tootu byla uložena do vašich konceptů</string>
<string name="send_post_notification_saved_content">Kopie vašeho příspěvku byla uložena do vašich konceptů</string>
<string name="action_compose_shortcut">Napsat</string>
<string name="error_no_custom_emojis">Vaše instance %s nemá žádná vlastní emoji</string>
<string name="emoji_style">Styl emoji</string>
<string name="system_default">Výchozí nastavení systému</string>
<string name="download_fonts">Musíte si nejprve stáhnout tyto sady emoji</string>
<string name="performing_lookup_title">Provádím prohledávání…</string>
<string name="expand_collapse_all_posts">Rozbalit/zabalit všechny příspěvky</string>
<string name="action_open_post">Otevřít toot</string>
<string name="restart_required">Je vyžadován restart aplikace</string>
<string name="expand_collapse_all_posts">Rozbalit/Sbalit všechny příspěvky</string>
<string name="action_open_post">Otevřít příspěvek</string>
<string name="restart_required">Je vyžadováno restartování aplikace</string>
<string name="restart_emoji">Pro použití těchto změn musíte restartovat aplikaci Yuito</string>
<string name="later">Později</string>
<string name="restart">Restartovat</string>
@ -326,7 +333,7 @@
<string name="profile_metadata_label_label">Označení</string>
<string name="profile_metadata_content_label">Obsah</string>
<string name="pref_title_absolute_time">Používat absolutní čas</string>
<string name="label_remote_account">Níže uvedené informace nemusejí zcela odrážet profil uživatele. Dotknutím otevřete celý profil v prohlížeči.</string>
<string name="label_remote_account">Níže uvedené informace nemusejí zcela odrážet profil uživatele. Dotknutím se otevřete celý profil v prohlížeči.</string>
<string name="unpin_action">Odepnout</string>
<string name="pin_action">Připnout</string>
<plurals name="favs">
@ -336,7 +343,7 @@
</plurals>
<plurals name="reblogs">
<item quantity="one"><b>%s</b> boost</item>
<item quantity="few"><b>%s</b> boost</item>
<item quantity="few"><b>%s</b> boosty</item>
<item quantity="other"><b>%s</b> boostů</item>
</plurals>
<string name="title_reblogged_by">Boostnuto uživatelem</string>
@ -345,35 +352,31 @@
<string name="conversation_2_recipients">%1$s a %2$s</string>
<string name="conversation_more_recipients">%1$s, %2$s a %3$d další</string>
<plurals name="max_tab_number_reached">
<item quantity="other">bylo dosaženo maxima %1$d panelů</item>
<item quantity="one">bylo dosaženo maxima %1$d panelu</item>
<item quantity="few">bylo dosaženo maxima %1$d panelů</item>
<item quantity="other"></item>
</plurals>
<string name="description_post_media"> Média %s
</string>
<string name="description_post_cw"> Varování o obsahu: %s
</string>
<string name="description_post_media_no_description_placeholder"> Žádný popis
</string>
<string name="description_post_reblogged"> Boostnutý
</string>
<string name="description_post_favourited"> Oblíbený
</string>
<string name="description_post_media">Média %s</string>
<string name="description_post_cw">Varování o obsahu: %s</string>
<string name="description_post_media_no_description_placeholder">Žádný popis</string>
<string name="description_post_reblogged">Boostnutý</string>
<string name="description_post_favourited">Oblíbený</string>
<string name="description_visibility_public">Veřejný</string>
<string name="description_visibility_unlisted">Neuvedený</string>
<string name="description_visibility_private">Pro sledující</string>
<string name="description_visibility_direct"> Přímý
</string>
<string name="description_visibility_direct">Přímý</string>
<string name="hint_list_name">Název seznamu</string>
<string name="edit_hashtag_hint">Hashtag bez #</string>
<string name="compose_shortcut_long_label">Napsat toot</string>
<string name="compose_shortcut_long_label">Napsat příspěvek</string>
<string name="compose_shortcut_short_label">Napsat</string>
<string name="notifications_clear">Vymazat</string>
<string name="notifications_clear">Vyčistit</string>
<string name="notifications_apply_filter">Filtrovat</string>
<string name="filter_apply">Použít</string>
<string name="pref_title_bot_overlay">Zobrazovat indikátor pro roboty</string>
<string name="notification_clear_text">Jste si jistý/á, že chcete trvale vymazat všechna vaše oznámení\?</string>
<string name="action_delete_and_redraft">Smazat a přepsat</string>
<string name="dialog_redraft_post_warning">Smazat a přepsat tento toot\?</string>
<string name="poll_info_format"> <!-- 15 hlasů • 1 hodin do konce --> %1$s • %2$s</string>
<string name="dialog_redraft_post_warning">Smazat a přepsat tento příspěvek\?</string>
<string name="poll_info_format"> <!-- 15 hlasů • 1 hodina do konce --> %1$s • %2$s</string>
<plurals name="poll_info_votes">
<item quantity="one">%s hlas</item>
<item quantity="few">%s hlasy</item>
@ -403,30 +406,30 @@
<item quantity="few">zbývá %d sekundy</item>
<item quantity="other">zbývá %d sekund</item>
</plurals>
<string name="pref_title_animate_gif_avatars">Animovat avatary GIF</string>
<string name="pref_title_animate_gif_avatars">Animovat GIF avatary</string>
<string name="description_poll">Anketa s volbami: %1$s, %2$s, %3$s, %4$s; %5$s</string>
<string name="title_domain_mutes">Skryté domény</string>
<string name="action_view_domain_mutes">Skryté domény</string>
<string name="action_mute_domain">Skrýt doménu %s</string>
<string name="confirmation_domain_unmuted">Doména %s odkryta</string>
<string name="confirmation_domain_unmuted">Skrytí domény %s bylo zrušeno</string>
<string name="mute_domain_warning">Jste si jistý/á, že chcete zablokovat vše z domény %s\? Z této domény neuvidíte obsah v žádné veřejné časové ose ani v oznámeních. Vaši sledující z této domény budou odstraněni.</string>
<string name="mute_domain_warning_dialog_ok">Skrýt celou doménu</string>
<string name="caption_notoemoji">Aktuální sada emoji od Googlu</string>
<string name="button_continue">Pokračovat</string>
<string name="button_back">Zpět</string>
<string name="button_done">Hotovo</string>
<string name="report_sent_success">\@%s úspěšně nahlášen/a</string>
<string name="hint_additional_info">Dodatečné komentáře</string>
<string name="report_sent_success">\@%s byla/a úspěšně nahlášen/a</string>
<string name="hint_additional_info">Další komentáře</string>
<string name="report_remote_instance">Přeposlat na %s</string>
<string name="failed_report">Nahlášení selhalo</string>
<string name="failed_fetch_posts">Stahování tootů neuspělo</string>
<string name="failed_fetch_posts">Stahování příspěvků selhalo</string>
<string name="report_description_1">Nahlášení bude zasláno moderátorovi vašeho serveru. Níže můžete uvést, proč tento účet nahlašujete:</string>
<string name="report_description_remote_instance">Tento účet je z jiného serveru. Chcete na něj také poslat anonymizovanou kopii\?</string>
<string name="report_description_remote_instance">Tento účet je z jiného serveru. Chcete na něj také poslat anonymizovanou kopii nahlášení\?</string>
<string name="pref_title_show_notifications_filter">Zobrazit filtr oznámení</string>
<string name="create_poll_title">Anketa</string>
<string name="duration_5_min">5 minut</string>
<string name="duration_30_min">30 minut</string>
<string name="duration_1_hour">1 hodinu</string>
<string name="duration_1_hour">1 hodina</string>
<string name="duration_6_hours">6 hodin</string>
<string name="duration_1_day">1 den</string>
<string name="duration_3_days">3 dny</string>
@ -435,13 +438,13 @@
<string name="poll_allow_multiple_choices">Lze zvolit více možností</string>
<string name="poll_new_choice_hint">Možnost %d</string>
<string name="edit_poll">Upravit</string>
<string name="title_scheduled_posts">Plánované tooty</string>
<string name="title_scheduled_posts">Naplánováné příspěvky</string>
<string name="action_edit">Upravit</string>
<string name="action_add_poll">Přidat anketu</string>
<string name="action_access_scheduled_posts">Plánované tooty</string>
<string name="action_schedule_post">Naplánovat toot</string>
<string name="action_access_scheduled_posts">Naplánované příspěvky</string>
<string name="action_schedule_post">Naplánované příspěvky</string>
<string name="action_reset_schedule">Obnovit</string>
<string name="pref_title_alway_open_spoiler">Vždy rozbalovat tooty označené varováními o obsahu</string>
<string name="pref_title_alway_open_spoiler">Vždy rozbalovat příspěvky označené varováními o obsahu</string>
<string name="filter_dialog_whole_word">Celé slovo</string>
<string name="filter_dialog_whole_word_description">Je-li klíčové slovo nebo fráze pouze alfanumerická, bude použita pouze, pokud odpovídá celému slovu</string>
<string name="title_accounts">Účty</string>
@ -460,7 +463,7 @@
<string name="pref_title_show_cards_in_timelines">Ukazovat náhledy k odkazům</string>
<string name="warning_scheduling_interval">Mastodon neumožňuje pracovat s intervalem menším než 5 minut.</string>
<string name="no_scheduled_posts">Zatím zde nemáte žádné naplánované statusy.</string>
<string name="no_drafts">Zatím zde nejsou žádné koncepty.</string>
<string name="no_drafts">Zatím zde nemáte žádné koncepty.</string>
<string name="pref_title_enable_swipe_for_tabs">Možnost přetahování prstem pro přechod mezi kartami</string>
<string name="list">Seznam</string>
<string name="add_hashtag_title">Přidat hashtag</string>
@ -471,21 +474,115 @@
<string name="about_powered_by_tusky">Powered by Tusky</string>
<string name="dialog_block_warning">Zablokovat @%s\?</string>
<string name="pref_main_nav_position_option_top">Nahoře</string>
<string name="action_unmute_domain">Odkrýt %s</string>
<string name="action_unmute_domain">Zrušit skrytí domény %s</string>
<string name="action_mute_notifications_desc">Skrýt oznámení od %s</string>
<string name="action_unmute_notifications_desc">Odkrýt oznámení od %s</string>
<string name="action_unmute_desc">Odkrýt %s</string>
<string name="dialog_mute_warning">Ztišit @%s\?</string>
<string name="action_unmute_notifications_desc">Zrušit skrytí oznámení od %s</string>
<string name="action_unmute_desc">Zrušit skrytí %s</string>
<string name="dialog_mute_warning">Skrýt @%s\?</string>
<string name="notification_follow_request_format">%s požádal/a aby vás mohl/a sledovat</string>
<string name="pref_title_confirm_reblogs">Zobrazit dialogové okno s potvrzením při boostování</string>
<string name="notification_subscription_format">%s právě vydal</string>
<string name="notification_subscription_format">%s právě zveřejnil/a příspěvek</string>
<string name="title_announcements">Oznámení</string>
<string name="title_login">Přihlášení</string>
<string name="notification_sign_up_format">%s se zaregistroval</string>
<string name="title_migration_relogin">Přihlaste se znovu pro oznámení</string>
<string name="error_could_not_load_login_page">Nepodařilo se načíst stránku přihlášení.</string>
<string name="drafts_post_failed_to_send">Tento příspěvek se nepodařilo poslat!</string>
<string name="notification_sign_up_format">%s se zaregistroval/a</string>
<string name="title_migration_relogin">Přihlaste se znovu pro push oznámení</string>
<string name="error_could_not_load_login_page">Nepodařilo se načíst přihlášovací stránku.</string>
<string name="drafts_post_failed_to_send">Tento příspěvek se nepodařilo odeslat!</string>
<string name="error_loading_account_details">Nepodařilo se načíst detaily účtu</string>
<string name="drafts_failed_loading_reply">Nepodařilo se načíst informace o odpovědi</string>
<string name="error_image_edit_failed">Obrázek se nepodařilo upravit.</string>
<plurals name="poll_info_people">
<item quantity="one">%s osoba</item>
<item quantity="few">%s lidi</item>
<item quantity="other">%s lidí</item>
</plurals>
<plurals name="poll_timespan_hours">
<item quantity="one">zbývá %d hodina</item>
<item quantity="few">zbývají %d hodiny</item>
<item quantity="other">zbývá %d hodin</item>
</plurals>
<plurals name="error_upload_max_media_reached">
<item quantity="one">Nemůžete nahrát více než %1$d mediální přílohu.</item>
<item quantity="few">Nemůžete nahrát více než %1$d mediální přílohy.</item>
<item quantity="other">Nemůžete nahrát více než %1$d mediálních příloh.</item>
</plurals>
<string name="delete_scheduled_post_warning">Smazat tento naplánovaný příspěvek\?</string>
<string name="notification_subscription_description">Upozornění na nový toot někoho, koho sledujete.</string>
<string name="instance_rule_info">Přihlášením souhlasíte s pravidly serveru %s.</string>
<string name="instance_rule_title">Pravidla serveru %s</string>
<string name="wellbeing_mode_notice">Některé informace, které mohou ovlivnit Vaši duševní pohodu, mohou být skryty. To zahrnuje:
\n
\n - Upozornění na boosty, oblíbené a sledování
\n - Počty boostů a oblíbení u příspěvků
\n - Statistiky sledujících a příspěvků na profilech
\n
\nPush oznámení nebudou ovlivněna, ale můžete si zkontrolovat jejich nastavení manuálně.</string>
<string name="dialog_push_notification_migration_other_accounts">Znovu jste se přihlásili ke svému aktuálnímu účtu, abyste aplikaci Tusky udělili oprávnění k odběru push. Stále však máte další účty, které tímto způsobem migrovány nebyly. Přepněte se na ně a znovu se přihlaste na jednom po druhém, abyste povolili podporu oznámení UnifiedPush.</string>
<string name="compose_save_draft_loses_media">Uložit koncept\? (Přílohy budou znovu nahrány, když obnovíte koncept.)</string>
<string name="set_focus_description">Klepnutím nebo přetažením kruhu vyberte ohnisko, které bude vždy viditelné v miniaturách.</string>
<string name="pref_title_confirm_favourites">Před oblíbením zobrazit dialog pro potvrzení</string>
<string name="pref_title_hide_top_toolbar">Skrýt nadpis horního panelu nástrojů</string>
<string name="wellbeing_hide_stats_profile">Skrýt kvantitativní statistiky profilů</string>
<string name="dialog_delete_list_warning">Opravdu chcete smazat seznam %s\?</string>
<string name="follow_requests_info">I když váš účet není uzamčen, zaměstnanci %1$s si myslí, že byste mohli chtít zkontrolovat žádosti o sledování z těchto účtů ručně.</string>
<string name="action_subscribe_account">Odebírat</string>
<string name="action_unsubscribe_account">Přestat odebírat</string>
<string name="tusky_compose_post_quicksetting_label">Vytvořit příspěvek</string>
<string name="draft_deleted">Koncept byl smazán</string>
<string name="error_multimedia_size_limit">Video a audio soubory nesmí překročit velikost %s MB.</string>
<string name="failed_to_pin">Připnutí se nezdařilo</string>
<string name="failed_to_unpin">Zrušení připnutí se nezdařilo</string>
<string name="pref_show_self_username_always">Vždy</string>
<string name="pref_show_self_username_never">Nikdy</string>
<string name="filter_expiration_format">%s (%s)</string>
<string name="action_edit_image">Upravit obrázek</string>
<string name="duration_14_days">14 dní</string>
<string name="duration_30_days">30 dní</string>
<string name="drafts_post_reply_removed">Příspěvek, na který jste připravili odpověď, byl odstraněn</string>
<string name="url_domain_notifier">%s (🔗 %s)</string>
<string name="action_set_focus">Nastavit bod zaostření</string>
<string name="error_failed_set_focus">Nepodařilo se nastavit zaostřovací bod</string>
<string name="tips_push_notification_migration">Znovu se přihlaste ke všem účtům, abyste povolili podporu push oznámení.</string>
<string name="dialog_push_notification_migration">Aby bylo možné používat push oznámení prostřednictvím UnifiedPush, Tusky potřebuje oprávnění k odběru oznámení na vašem serveru Mastodon. To vyžaduje opětovné přihlášení ke změně rozsahů OAuth udělených aplikaci Tusky. Použitím možnosti opětovného přihlášení zde nebo v předvolbách účtu zachováte všechny vaše místní koncepty a mezipaměť.</string>
<string name="action_add_reaction">přidat reakci</string>
<string name="pref_title_notification_filter_updates">příspěvek, se kterým jsem interagoval/a, je upraven</string>
<string name="pref_title_notification_filter_sign_ups">někdo se zaregistroval</string>
<string name="pref_title_notification_filter_subscriptions">někdo, ke komu jsem přihlášen/a, zveřejnil nový příspěvek</string>
<string name="pref_show_self_username_disambiguate">Když je přihlášeno více účtů</string>
<string name="notification_subscription_name">Nové příspěvky</string>
<string name="notification_sign_up_name">Registrace</string>
<string name="notification_sign_up_description">Oznámení o nových uživatelích</string>
<string name="notification_update_name">Úpravy příspěvků</string>
<string name="notification_update_description">Oznámení, když je upraven příspěvek, se kterým jste interagovala, je upraven</string>
<string name="post_media_audio">Audio</string>
<string name="post_media_attachments">Přílohy</string>
<string name="status_count_one_plus">1+</string>
<string name="description_post_language">Jazyk příspěvku</string>
<string name="label_duration">Doba trvání</string>
<string name="duration_indefinite">Na neurčito</string>
<string name="duration_60_days">60 dní</string>
<string name="duration_90_days">90 dní</string>
<string name="duration_180_days">180 dní</string>
<string name="duration_365_days">365 dní</string>
<string name="duration_no_change">(Beze změny)</string>
<string name="no_announcements">Nejsou zde žádná oznámení.</string>
<string name="pref_title_show_self_username">Zobrazit uživatelské jméno v panelech nástrojů</string>
<string name="account_note_hint">Váše soukromá poznámka o tomto účtu.</string>
<string name="account_note_saved">Uloženo!</string>
<string name="pref_title_wellbeing_mode">Pohoda</string>
<string name="review_notifications">Zkontrolovat oznámení</string>
<string name="limit_notifications">Omezit upozornění na časové ose</string>
<string name="wellbeing_hide_stats_posts">Skrýt kvantitativní statistiky příspěvků</string>
<string name="account_date_joined">Připojil/a se %1$s</string>
<string name="saving_draft">Koncept se ukládá…</string>
<string name="error_following_hashtag_format">Chyba při sledování #%s</string>
<string name="error_unfollowing_hashtag_format">Chyba při rušení sledování #%s</string>
<string name="notification_update_format">%s upravil/a svůj příspěvek</string>
<string name="action_unbookmark">Odebrat záložku</string>
<string name="action_delete_conversation">Smazat konverzaci</string>
<string name="action_dismiss">Zavřít</string>
<string name="action_details">Podrobnosti</string>
<string name="dialog_delete_conversation_warning">Smazat tuto konverzaci\?</string>
<string name="pref_title_notification_filter_follow_requests">Požádáno o sledování</string>
<string name="pref_title_animate_custom_emojis">Animovat vlastní emotikony</string>
</resources>

View File

@ -20,7 +20,7 @@
<string name="title_notifications">Hysbysiadau</string>
<string name="title_public_local">Lleol</string>
<string name="title_public_federated">Ffedereiddwyd</string>
<string name="title_view_thread">Edau</string>
<string name="title_view_thread">Neges</string>
<string name="title_posts">Negeseuon</string>
<string name="title_posts_with_replies">Gydag ymatebion</string>
<string name="title_follows">Dilyniadau</string>
@ -32,7 +32,7 @@
<string name="title_edit_profile">Golygu\'ch proffil</string>
<string name="title_drafts">Drafftiau</string>
<string name="title_licenses">Trwyddedau</string>
<string name="post_boosted_format">%s wedi\'u hybu</string>
<string name="post_boosted_format">Wedi\'i hybu gan %s</string>
<string name="post_sensitive_media_title">Cynnwys sensitif</string>
<string name="post_media_hidden_title">Cyfryngau wedi\'u cudd</string>
<string name="post_sensitive_media_directions">Cliciwch i weld</string>
@ -41,9 +41,9 @@
<string name="post_content_show_more">Chwyddo</string>
<string name="post_content_show_less">Lleihau</string>
<string name="footer_empty">Dim byd yma. Tynnwch lawr i adnewyddu!</string>
<string name="notification_reblog_format">%s wedi hybu\'ch post</string>
<string name="notification_favourite_format">%s wedi hoffi\'ch post</string>
<string name="notification_follow_format">%s wedi\'ch dilyn chi</string>
<string name="notification_reblog_format">Mae %s wedi hybu\'ch post</string>
<string name="notification_favourite_format">Mae %s wedi hoffi\'ch post</string>
<string name="notification_follow_format">Mae %s wedi\'ch dilyn chi</string>
<string name="report_username_format">Adrodd @%s</string>
<string name="report_comment_hint">Sylwadau ychwanegol?</string>
<string name="action_quick_reply">Ateb Cyflym</string>
@ -309,7 +309,7 @@
<string name="action_mute_conversation">Tewi sgwrs</string>
<string name="notifications_apply_filter">Hidlo</string>
<string name="notification_poll_description">Hysbysiadau am bolau sydd wedi cwblhau</string>
<string name="account_date_joined">Ymunwyd %1$s</string>
<string name="account_date_joined">Ymunodd %1$s</string>
<string name="no_scheduled_posts">Does gennych ddim negeseuon arfaethedig.</string>
<string name="filter_expiration_format">%s (%s)</string>
<string name="notification_clear_text">Ydych chi\'n siŵr eich bod chi am glirio\'ch holl hysbysiadau yn barhaol\?</string>

View File

@ -10,12 +10,12 @@
<string name="error_authorization_denied">Autorisierung wurde abgelehnt.</string>
<string name="error_retrieving_oauth_token">Es konnte kein Login-Token abgerufen werden.</string>
<string name="error_compose_character_limit">Der Beitrag ist zu lang!</string>
<string name="error_media_upload_type">Dieser Dateityp darf nicht hochgeladen werden.</string>
<string name="error_media_upload_type">Dieser Dateityp kann nicht hochgeladen werden.</string>
<string name="error_media_upload_opening">Die Datei konnte nicht geöffnet werden.</string>
<string name="error_media_upload_permission">Eine Leseberechtigung wird für das Hochladen der Mediendatei benötigt.</string>
<string name="error_media_upload_permission">Berechtigung für Zugriff auf Mediendateien benötigt.</string>
<string name="error_media_download_permission">Eine Berechtigung wird zum Speichern des Mediums benötigt.</string>
<string name="error_media_upload_image_or_video">Bilder und Videos können nicht an den gleichen Beitrag angehängt werden.</string>
<string name="error_media_upload_sending">Die Mediendatei konnte nicht hochgeladen werden.</string>
<string name="error_media_upload_sending">Das Hochladen ist gescheitert.</string>
<string name="error_sender_account_gone">Fehler beim Senden des Beitrags.</string>
<string name="title_home">Start</string>
<string name="title_notifications">Benachrichtigungen</string>
@ -25,7 +25,7 @@
<string name="title_tab_preferences">Tabs</string>
<string name="title_view_thread">Konversation</string>
<string name="title_posts">Beiträge</string>
<string name="title_posts_with_replies">mit Antworten</string>
<string name="title_posts_with_replies">Mit Antworten</string>
<string name="title_posts_pinned">Angeheftet</string>
<string name="title_follows">Folgt</string>
<string name="title_followers">Folgende</string>
@ -43,32 +43,32 @@
<string name="post_sensitive_media_directions">Zum Anzeigen tippen</string>
<string name="post_content_warning_show_more">Zeige mehr</string>
<string name="post_content_warning_show_less">Zeige weniger</string>
<string name="post_content_show_more">Mehr</string>
<string name="post_content_show_less">Weniger</string>
<string name="post_content_show_more">Ausklappen</string>
<string name="post_content_show_less">Einklappen</string>
<string name="message_empty">Hier ist nichts.</string>
<string name="footer_empty">Noch keine Beiträge hier! Ziehe nach unten um zu aktualisieren!</string>
<string name="notification_reblog_format">%s teilte deinen Beitrag</string>
<string name="notification_favourite_format">%s favorisierte deinen Beitrag</string>
<string name="notification_follow_format">%s folgt dir</string>
<string name="report_username_format">\@%s melden</string>
<string name="report_comment_hint">Irgendwelche Anmerkungen?</string>
<string name="report_comment_hint">Zusätzliche Anmerkungen\?</string>
<string name="action_quick_reply">Schnell antworten</string>
<string name="action_reply">Antworten</string>
<string name="action_reblog">Boosten</string>
<string name="action_unreblog">Boost entfernen</string>
<string name="action_reblog">Teilen</string>
<string name="action_unreblog">Teilen rückgängig machen</string>
<string name="action_favourite">Favorisieren</string>
<string name="action_unfavourite">Favorisierung entfernen</string>
<string name="action_more">Mehr</string>
<string name="action_compose">Beitrag erstellen</string>
<string name="action_login">Mit Mastodon anmelden</string>
<string name="action_logout">Ausloggen</string>
<string name="action_logout_confirm">Bist du sicher, dass du dich aus dem Konto %1$s ausloggen möchtest\?</string>
<string name="action_logout_confirm">Bist du sicher, dass du dich vom Konto %1$s abmelden möchtest\?</string>
<string name="action_follow">Folgen</string>
<string name="action_unfollow">Entfolgen</string>
<string name="action_block">Blockieren</string>
<string name="action_unblock">Entblockieren</string>
<string name="action_hide_reblogs">Geteilte Beiträge verbergen</string>
<string name="action_show_reblogs">Zeige Boosts</string>
<string name="action_show_reblogs">Zeige geteilte Beiträge</string>
<string name="action_report">Melden</string>
<string name="action_delete">Löschen</string>
<string name="action_send">TRÖT</string>
@ -116,12 +116,12 @@
<string name="download_image">%1$s heruntergeladen</string>
<string name="action_copy_link">Link kopieren</string>
<string name="action_open_as">Öffne als %s</string>
<string name="action_share_as">Teilen als </string>
<string name="send_post_link_to">Beitragslink teilen…</string>
<string name="send_post_content_to">Beitragsinhalt teilen…</string>
<string name="send_media_to">Mediendatei teilen…</string>
<string name="action_share_as">Teilen als </string>
<string name="send_post_link_to">Beitragslink teilen </string>
<string name="send_post_content_to">Beitragsinhalt teilen </string>
<string name="send_media_to">Mediendatei teilen </string>
<string name="confirmation_reported">Gesendet!</string>
<string name="confirmation_unblocked">entblockt</string>
<string name="confirmation_unblocked">Benutzer entblockt</string>
<string name="confirmation_unmuted">Stummschaltung aufgehoben</string>
<string name="post_sent">Gesendet!</string>
<string name="post_sent_long">Antwort erfolgreich gesendet.</string>
@ -130,13 +130,13 @@
<string name="hint_content_warning">Inhaltswarnung</string>
<string name="hint_display_name">Anzeigename</string>
<string name="hint_note">Über mich</string>
<string name="hint_search">Mastodon durchsuchen</string>
<string name="hint_search">Suchen </string>
<string name="search_no_results">Keine Ergebnisse</string>
<string name="label_quick_reply">Antworten…</string>
<string name="label_quick_reply">Antworten </string>
<string name="label_avatar">Profilbild</string>
<string name="label_header">Titelbild</string>
<string name="link_whats_an_instance">Was ist eine Instanz?</string>
<string name="login_connection">Verbinden </string>
<string name="link_whats_an_instance">Was ist eine Instanz\?</string>
<string name="login_connection">Verbinde </string>
<string name="dialog_whats_an_instance">Die Adresse einer Instanz oder Domain kann
hier eingegeben werden, wie z.B. mastodon.social, icosahedron.website, social.tchncs.de, und
<a href="https://instances.social">mehr!</a>
@ -147,7 +147,7 @@
\n\nWeitere Informationen gibt es auf <a href="https://joinmastodon.org">joinmastodon.org</a>.
</string>
<string name="dialog_title_finishing_media_upload">Stelle Medienupload fertig</string>
<string name="dialog_message_uploading_media">Lade hoch </string>
<string name="dialog_message_uploading_media">Lade hoch </string>
<string name="dialog_download_image">Herunterladen</string>
<string name="dialog_message_cancel_follow_request">Folgeanfrage zurückziehen?</string>
<string name="dialog_unfollow_warning">Willst du diesem Profil wirklich nicht mehr folgen?</string>
@ -165,8 +165,8 @@
<string name="pref_title_notification_filters">Benachrichtigen wenn</string>
<string name="pref_title_notification_filter_mentions">Ich erwähnt werde</string>
<string name="pref_title_notification_filter_follows">Mir jemand folgt</string>
<string name="pref_title_notification_filter_reblogs">Jemand meine Posts teilt</string>
<string name="pref_title_notification_filter_favourites">Jemandem meine Posts gefallen</string>
<string name="pref_title_notification_filter_reblogs">Jemand meine Beiträge teilt</string>
<string name="pref_title_notification_filter_favourites">Jemandem meine Beiträge gefallen</string>
<string name="pref_title_appearance_settings">Aussehen</string>
<string name="pref_title_app_theme">App-Thema</string>
<string name="pref_title_timelines">Zeitleisten</string>
@ -262,14 +262,17 @@
<string name="compose_active_account_description">Veröffentlichen als %1$s</string>
<string name="error_failed_set_caption">Fehler beim Speichern der Beschreibung</string>
<plurals name="hint_describe_for_visually_impaired">
<item quantity="other">Für Menschen mit Sehbehinderung beschreiben\n(%d Zeichen)</item>
<item quantity="one">Für Mensch mit Sehbehinderung beschreiben
\n(%d Zeichen)</item>
<item quantity="other">Für Menschen mit Sehbehinderung beschreiben
\n(%d Zeichen)</item>
</plurals>
<string name="action_set_caption">Beschreibung eingeben</string>
<string name="action_remove">Entfernen</string>
<string name="lock_account_label">Gesperrtes Profil</string>
<string name="lock_account_label_description">Wer dir folgen möchte, muss um deine Erlaubnis bitten</string>
<string name="compose_save_draft">Entwurf speichern?</string>
<string name="send_post_notification_title">Beitrag senden</string>
<string name="send_post_notification_title">Sende Beitrag </string>
<string name="send_post_notification_error_title">Fehler beim Senden</string>
<string name="send_post_notification_channel_name">Beiträge senden</string>
<string name="send_post_notification_cancel_title">Senden abgebrochen</string>
@ -279,7 +282,7 @@
<string name="emoji_style">Emoji-Stil</string>
<string name="system_default">System-Standard</string>
<string name="download_fonts">Du musst diese Emoji-Sets zunächst herunterladen</string>
<string name="performing_lookup_title">Nachschlagen</string>
<string name="performing_lookup_title">Schlage nach </string>
<string name="expand_collapse_all_posts">Alle Beiträge aus-/einklappen</string>
<string name="action_open_post">Beitrag öffnen</string>
<string name="restart_required">App-Neustart erforderlich</string>
@ -419,7 +422,7 @@
<string name="poll_allow_multiple_choices">Mehrere Möglichkeiten</string>
<string name="poll_new_choice_hint">Möglichkeit %d</string>
<string name="title_scheduled_posts">Geplante Beiträge</string>
<string name="action_edit">Editieren</string>
<string name="action_edit">Bearbeiten</string>
<string name="action_access_scheduled_posts">Geplante Beiträge</string>
<string name="action_schedule_post">Plane Beitrag</string>
<string name="action_reset_schedule">Zurücksetzen</string>
@ -430,7 +433,7 @@
<string name="description_post_bookmarked">Als Lesezeichen gespeichert</string>
<string name="select_list_title">Liste auswählen</string>
<string name="list">Liste</string>
<string name="post_lookup_error_format">Fehler beim Nachschlagen von Post %s</string>
<string name="post_lookup_error_format">Fehler beim Nachschlagen von Beitrag %s</string>
<string name="no_drafts">Du hast keine Entwürfe.</string>
<string name="no_scheduled_posts">Du hast keine geplanten Beiträge.</string>
<string name="warning_scheduling_interval">Das Datum des geplanten Toots muss mindestens 5 Minuten in der Zukunft liegen.</string>
@ -490,7 +493,7 @@
<string name="notification_subscription_name">Neue Beiträge</string>
<string name="pref_title_animate_custom_emojis">GIF-Emojis animieren</string>
<string name="pref_title_notification_filter_subscriptions">Jemand, den ich abonniert habe, hat etwas Neues veröffentlicht</string>
<string name="notification_subscription_format">%s hat gerade etwas gepostet</string>
<string name="notification_subscription_format">%s hat gerade etwas veröffentlicht</string>
<string name="abbreviated_minutes_ago">%d Min.</string>
<string name="review_notifications">Benachrichtigungen überprüfen</string>
<string name="wellbeing_mode_notice">Informationen, die dein Wohlbefinden beeinflussen könnten, werden versteckt. Das beinhaltet
@ -502,7 +505,7 @@
\nPush-Benachrichtigungen sind nicht betroffen, aber du kannst diese manuell überprüfen.</string>
<string name="follow_requests_info">Auch wenn dein Konto nicht gesperrt ist, haben die Admins von %1$s gedacht, dass es besser wäre diese Folgenden manuell zu bestätigen.</string>
<string name="wellbeing_hide_stats_profile">Keine Statistiken auf Profilen zeigen</string>
<string name="wellbeing_hide_stats_posts">Keine Statistiken in Posts zeigen</string>
<string name="wellbeing_hide_stats_posts">Keine Statistiken in Beiträgen zeigen</string>
<string name="limit_notifications">Timeline-Benachrichtigungen einschränken</string>
<string name="action_subscribe_account">Abonnieren</string>
<string name="action_unsubscribe_account">nicht mehr abonnieren</string>
@ -540,12 +543,31 @@
<string name="tips_push_notification_migration">Melde alle Konten neu an, um die Unterstützung für Push-Benachrichtigungen zu aktivieren.</string>
<string name="account_date_joined">%1$s beigetreten</string>
<string name="status_count_one_plus">1+</string>
<string name="status_created_at_now">Jetzt</string>
<string name="error_loading_account_details">Fehler beim Laden der Kontodetails</string>
<string name="action_edit_image">Bild bearbeiten</string>
<string name="action_details">Details</string>
<string name="error_image_edit_failed">Das Bild konnte nicht bearbeitet werden.</string>
<string name="saving_draft">Speichere den Entwurf…</string>
<string name="saving_draft">Speichere Entwurf </string>
<string name="error_multimedia_size_limit">Video- oder Tondateien dürfen nicht grösser als %s MB sein.</string>
<string name="error_following_hashtag_format">#%s folgen fehlgeschlagen</string>
<string name="error_unfollowing_hashtag_format">#%s entfolgen fehlgeschlagen</string>
<string name="delete_scheduled_post_warning">Diesen geplanten Beitrag löschen\?</string>
<string name="instance_rule_title">%s-Regeln</string>
<string name="instance_rule_info">Mit dem Anmelden stimmst du den Regeln von %s zu.</string>
<string name="set_focus_description">Tippe oder ziehe den Kreis auf die Stelle, die in Vorschaubildern in der Mitte sein soll.</string>
<string name="compose_save_draft_loses_media">Entwurf speichern\? (Anhänge werden erneut hochgeladen, sobald du den Entwurf wiederherstellst.)</string>
<string name="duration_no_change">(Keine Änderung)</string>
<string name="pref_title_show_self_username">Benutzername in Hauptnavigation anzeigen</string>
<string name="failed_to_pin">Pinnen fehlgeschlagen</string>
<string name="failed_to_unpin">Lösen fehlgeschlagen</string>
<string name="pref_show_self_username_always">Immer</string>
<string name="pref_show_self_username_disambiguate">Wenn mit mehreren Konten angemeldet</string>
<string name="pref_show_self_username_never">Niemals</string>
<string name="filter_expiration_format">%s (%s)</string>
<string name="description_post_language">Sprache des Beitrags</string>
<string name="url_domain_notifier">%s (🔗 %s)</string>
<string name="error_failed_set_focus">Setzen des Fokuspunktes fehlgeschlagen</string>
<string name="action_set_focus">Fokuspunkt setzen</string>
<string name="action_add_reaction">Reaktion hinzufügen</string>
</resources>

View File

@ -148,11 +148,11 @@
\n
\nPliaj informoj troviĝas ĉe <a href="https://joinmastodon.org">joinmastodon.org</a>. </string>
<string name="dialog_title_finishing_media_upload">Finante alŝuto de aŭdovidaĵojn</string>
<string name="dialog_message_uploading_media">Alŝutante</string>
<string name="dialog_message_uploading_media">Alŝutado</string>
<string name="dialog_download_image">Elŝuti</string>
<string name="dialog_message_cancel_follow_request">Nuligi peton de sekvado?</string>
<string name="dialog_unfollow_warning">Ne plu sekvi?</string>
<string name="dialog_delete_post_warning">Forigi ĉi tiun mesaĝon?</string>
<string name="dialog_message_cancel_follow_request">Ĉu nuligi peton de sekvado\?</string>
<string name="dialog_unfollow_warning">Ĉu ne plu sekvi\?</string>
<string name="dialog_delete_post_warning">Ĉu forigi ĉi tiun mesaĝon\?</string>
<string name="visibility_public">Publika: afiŝi en publikaj tempolinioj</string>
<string name="visibility_unlisted">Nelistigita: Ne afiŝi en publikaj tempolinioj</string>
<string name="visibility_private">Nur por sekvantoj: Afiŝi nur al sekvantoj</string>
@ -163,7 +163,7 @@
<string name="pref_title_notification_alert_sound">Sciigi per sono</string>
<string name="pref_title_notification_alert_vibrate">Sciigi per vibro</string>
<string name="pref_title_notification_alert_light">Sciigi per lumo</string>
<string name="pref_title_notification_filters">Sciigi al mi kiam</string>
<string name="pref_title_notification_filters">Sciigi al mi, kiam</string>
<string name="pref_title_notification_filter_mentions">iu mencias min</string>
<string name="pref_title_notification_filter_follows">iu sekvas min</string>
<string name="pref_title_notification_filter_reblogs">miaj mesaĝoj estas diskonigitaj</string>
@ -176,10 +176,10 @@
<string name="app_theme_light">Hela</string>
<string name="app_theme_black">Nigra</string>
<string name="app_theme_auto">Aŭtomata laŭ la horo</string>
<string name="app_theme_system">Uzi sisteman temon</string>
<string name="app_theme_system">Uzi sisteman etoson</string>
<string name="pref_title_browser_settings">Retumilo</string>
<string name="pref_title_custom_tabs">Uzi la integritan retumilon</string>
<string name="pref_title_hide_follow_button">Kâsi butonon de verko dum rulumado</string>
<string name="pref_title_hide_follow_button">Ki butonon de verko dum rulumado</string>
<string name="pref_title_language">Lingvo</string>
<string name="pref_title_post_filter">Filtrado de tempolinioj</string>
<string name="pref_title_post_tabs">Langetoj</string>
@ -187,14 +187,14 @@
<string name="pref_title_show_replies">Montri la respondojn</string>
<string name="pref_title_show_media_preview">Elŝuti antaŭvidojn de aŭdovidaĵoj</string>
<string name="pref_title_proxy_settings">Prokurilo</string>
<string name="pref_title_http_proxy_settings">HTTP prokurilo</string>
<string name="pref_title_http_proxy_enable">Ebligi HTTP prokurilon</string>
<string name="pref_title_http_proxy_server">Adreso de HTTP prokurilo</string>
<string name="pref_title_http_proxy_port">Pordo de HTTP prokurilo</string>
<string name="pref_title_http_proxy_settings">HTTP-prokurilo</string>
<string name="pref_title_http_proxy_enable">Ebligi HTTP-prokurilon</string>
<string name="pref_title_http_proxy_server">Adreso de HTTP-prokurilo</string>
<string name="pref_title_http_proxy_port">Pordo de HTTP-prokurilo</string>
<string name="pref_default_post_privacy">Dekomenca privateco de mesaĝoj</string>
<string name="pref_default_media_sensitivity">Ĉiam marki aŭdovidaĵojn kiel tiklaj</string>
<string name="pref_default_media_sensitivity">Ĉiam marki aŭdovidaĵojn kiel tiklajn</string>
<string name="pref_publishing">Publikigante (sinkronigita kun la servilo)</string>
<string name="pref_failed_to_sync">Sinkronigo de la preferoj malsukcesis</string>
<string name="pref_failed_to_sync">Sinkronigo de la agordoj malsukcesis</string>
<string name="post_privacy_public">Publika</string>
<string name="post_privacy_unlisted">Nelistigita</string>
<string name="post_privacy_followers_only">Nur por sekvantoj</string>
@ -205,16 +205,16 @@
<string name="post_text_size_large">Granda</string>
<string name="post_text_size_largest">La plej granda</string>
<string name="notification_mention_name">Novaj mencioj</string>
<string name="notification_mention_descriptions">Sciigoj pri novajn menciojn</string>
<string name="notification_mention_descriptions">Sciigoj pri novaj mencioj</string>
<string name="notification_follow_name">Novaj sekvantoj</string>
<string name="notification_follow_description">Sciigoj pri novajn sekvantojn</string>
<string name="notification_follow_description">Sciigoj pri novaj sekvantoj</string>
<string name="notification_boost_name">Diskonigoj</string>
<string name="notification_boost_description">Sciigoj kiam viaj mesaĝoj estas diskonigita</string>
<string name="notification_boost_description">Sciigoj, kiam viaj mesaĝoj estas diskonigitaj</string>
<string name="notification_favourite_name">Stelumoj</string>
<string name="notification_favourite_description">Sciigoj kiam viaj mesaĝoj estas stelumitaj</string>
<string name="notification_favourite_description">Sciigoj, kiam viaj mesaĝoj estas stelumitaj</string>
<string name="notification_mention_format">%s menciis vin</string>
<string name="notification_summary_large">%1$s, %2$s, %3$s kaj %4$d aliaj</string>
<string name="notification_summary_medium">%1$s, %2$s, kaj %3$s</string>
<string name="notification_summary_medium">%1$s, %2$s kaj %3$s</string>
<string name="notification_summary_small">%1$s kaj %2$s</string>
<plurals name="notification_title_summary">
<item quantity="one">%d nova interago</item>
@ -232,12 +232,10 @@
to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html
* the url can be changed to link to the localized version of the license.
-->
<string name="about_project_site"> Paĝaro de projekto:\n
https://accelf.net/yuito
</string>
<string name="about_bug_feature_request_site"> Raportoj de cimo kaj petoj de funkcio:\n
https://github.com/accelforce/Yuito/issues
</string>
<string name="about_project_site">Paĝaro de la projekto:
\n https://accelf.net/yuito</string>
<string name="about_bug_feature_request_site">Raportoj de cimoj kaj petoj de funkcioj:
\n https://github.com/accelforce/Yuito/issues</string>
<string name="about_tusky_account">Profilo de Yuito</string>
<string name="post_share_content">Konigi enhavon de la mesaĝo</string>
<string name="post_share_link">Konigi ligilon al mesaĝo</string>
@ -245,11 +243,11 @@
<string name="post_media_video">Video</string>
<string name="state_follow_requested">Sekvado petita</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">en %dj</string>
<string name="abbreviated_in_days">en %dt</string>
<string name="abbreviated_in_hours">en %dh</string>
<string name="abbreviated_in_minutes">en %dm</string>
<string name="abbreviated_in_seconds">en %ds</string>
<string name="abbreviated_in_years">post %dj</string>
<string name="abbreviated_in_days">post %dt</string>
<string name="abbreviated_in_hours">post %dh</string>
<string name="abbreviated_in_minutes">post %dm</string>
<string name="abbreviated_in_seconds">post %ds</string>
<string name="abbreviated_years_ago">%dj</string>
<string name="abbreviated_days_ago">%dt</string>
<string name="abbreviated_hours_ago">%dh</string>
@ -260,15 +258,15 @@
<string name="title_media">Aŭdovidaĵoj</string>
<string name="replying_to">Respondo al @%s</string>
<string name="load_more_placeholder_text">ŝarĝi pli</string>
<string name="pref_title_public_filter_keywords">Publikaj liniotempoj</string>
<string name="pref_title_public_filter_keywords">Publikaj tempolinioj</string>
<string name="pref_title_thread_filter_keywords">Konversacioj</string>
<string name="filter_addition_dialog_title">Aldoni filtrilon</string>
<string name="filter_edit_dialog_title">Redakti filtrilon</string>
<string name="filter_dialog_remove_button">Forigi</string>
<string name="filter_dialog_update_button">Akualigi</string>
<string name="filter_dialog_update_button">Aktualigi</string>
<string name="filter_add_description">Frazo filtrota</string>
<string name="add_account_name">Aldoni konton</string>
<string name="add_account_description">Aldoni novan Mastodon konton</string>
<string name="add_account_description">Aldoni novan Mastodon-konton</string>
<string name="action_lists">Listoj</string>
<string name="title_lists">Listoj</string>
<string name="error_create_list">Ne povis krei la liston</string>
@ -278,30 +276,32 @@
<string name="action_rename_list">Ŝanĝi la nomon de la listo</string>
<string name="action_delete_list">Forigi la liston</string>
<string name="action_edit_list">Redakti la liston</string>
<string name="hint_search_people_list">Serĉi homojn ke vi sekvas</string>
<string name="hint_search_people_list">Serĉi homojn, kiujn vi sekvas</string>
<string name="action_add_to_list">Aldoni konton al la listo</string>
<string name="action_remove_from_list">Forigi konton el la listo</string>
<string name="compose_active_account_description">Afiŝi per konto %1$s</string>
<string name="error_failed_set_caption">Redakto de apudskribo malsukcesis</string>
<plurals name="hint_describe_for_visually_impaired">
<item quantity="other">Priskribi por misvidantaj homoj\n(%d signoj maksimume)</item>
<item quantity="one"></item>
<item quantity="other">Priskribi por vide handikapitaj homoj
\n(%d signoj maksimume)</item>
</plurals>
<string name="action_set_caption">Redakti apudskribon</string>
<string name="action_remove">Forigi</string>
<string name="lock_account_label">Ŝlosi konton</string>
<string name="lock_account_label_description">Vi devas permane rajtigi sekvantojn</string>
<string name="compose_save_draft">Konservi malneton?</string>
<string name="send_post_notification_title">Sendante la mesaĝo…</string>
<string name="compose_save_draft">Ĉu konservi malneton\?</string>
<string name="send_post_notification_title">Sendado de la mesaĝo…</string>
<string name="send_post_notification_error_title">Eraro dum sendo de la mesaĝo</string>
<string name="send_post_notification_channel_name">Sendante la mesaĝoj</string>
<string name="send_post_notification_channel_name">Sendado de la mesaĝoj</string>
<string name="send_post_notification_cancel_title">Sendo nuligita</string>
<string name="send_post_notification_saved_content">Kopio de la mesaĝo estis konservita en viaj malnetoj</string>
<string name="action_compose_shortcut">Verki</string>
<string name="error_no_custom_emojis">Via nodo %s ne havas proprajn emoĝiojn</string>
<string name="emoji_style">Stilo de emoĝioj</string>
<string name="system_default">Sistema valoro</string>
<string name="download_fonts">Vi unue devos elŝuti ĉi tiujn emoĝiarojn</string>
<string name="performing_lookup_title">Serĉante</string>
<string name="system_default">El la sistemo</string>
<string name="download_fonts">Vi unue devos elŝuti ĉi tiun emoĝiaron</string>
<string name="performing_lookup_title">Serĉado</string>
<string name="expand_collapse_all_posts">Pligrandigi/malgrandigi ĉiujn mesaĝojn</string>
<string name="action_open_post">Malfermi mesaĝon</string>
<string name="restart_required">Restartigo necesas</string>
@ -309,15 +309,15 @@
<string name="later">Poste</string>
<string name="restart">Restartigi</string>
<string name="caption_systememoji">Dekomenca emoĝiaro de via aparato</string>
<string name="caption_blobmoji">La emoĝioj «Blob» konataj el Android 4.47.1</string>
<string name="caption_blobmoji">La emoĝioj «Blob» konataj de Android 4.47.1</string>
<string name="caption_twemoji">Norma emoĝiaro de Mastodon</string>
<string name="download_failed">Elŝuto malsukcesis</string>
<string name="profile_badge_bot_text">Roboto</string>
<string name="account_moved_description">%1$s moviĝis al:</string>
<string name="reblog_private">Diskonigi al la originala atentaro</string>
<string name="unreblog_private">Eksdiskonigi</string>
<string name="unreblog_private">Maldiskonigi</string>
<string name="license_description">Yuito enhavas kodon kaj risurcojn el la sekvantaj malfermitkodaj projetkoj:</string>
<string name="license_apache_2">Laŭ la permesilo «Apache License» (kopio sube)</string>
<string name="license_apache_2">Laŭ la permesilo «Apache» (kopio sube)</string>
<string name="license_cc_by_4">CC-BY 4.0</string>
<string name="license_cc_by_sa_4">CC-BY-SA 4.0</string>
<string name="profile_metadata_label">Profilaj metadatumoj</string>
@ -325,7 +325,7 @@
<string name="profile_metadata_label_label">Etikedo</string>
<string name="profile_metadata_content_label">Enhavo</string>
<string name="pref_title_absolute_time">Uzi absolutan tempon</string>
<string name="label_remote_account">Subaj informoj povas nekomplete prezenti la profilon de la uzanto. Presi por malfermi la kompletan profilon en retumilo.</string>
<string name="label_remote_account">Subaj informoj povas nekomplete prezenti la profilon de la uzanto. Tuŝi por malfermi la kompletan profilon en retumilo.</string>
<string name="unpin_action">Depingli</string>
<string name="pin_action">Alpingli</string>
<plurals name="favs">
@ -337,7 +337,7 @@
<item quantity="other">&lt;b&gt;%s&lt;/b&gt; Diskonigoj</item>
</plurals>
<string name="title_reblogged_by">Diskonigita de</string>
<string name="title_favourited_by">Stelumita per</string>
<string name="title_favourited_by">Stelumita de</string>
<string name="conversation_1_recipients">%1$s</string>
<string name="conversation_2_recipients">%1$s kaj %2$s</string>
<string name="conversation_more_recipients">%1$s, %2$s kaj %3$d aliaj</string>
@ -363,19 +363,19 @@
<string name="description_visibility_direct"> Rekta </string>
<string name="hint_list_name">Nomo de la listo</string>
<string name="action_delete_and_redraft">Forigi kaj reskribi</string>
<string name="dialog_redraft_post_warning">Ĉu forigi kaj reskribi ĉi-tiun mesaĝon\?</string>
<string name="dialog_redraft_post_warning">Ĉu forigi kaj reskribi ĉi tiun mesaĝon\?</string>
<string name="pref_title_notification_filter_poll">enketoj finiĝis</string>
<string name="pref_title_bot_overlay">Montri indikilon por robotoj</string>
<string name="pref_title_animate_gif_avatars">Moviĝi GIF profilbildojn</string>
<string name="pref_title_bot_overlay">Montri indikilon pri robotoj</string>
<string name="pref_title_animate_gif_avatars">Ebligi GIF-profilbildojn</string>
<string name="notification_poll_name">Enketoj</string>
<string name="notification_poll_description">Sciigoj pri enketoj kiuj finiĝis</string>
<string name="notification_poll_description">Sciigoj pri enketoj, kiuj finiĝis</string>
<string name="edit_hashtag_hint">Kradvorto sen #</string>
<string name="notifications_clear">Viŝi</string>
<string name="notifications_apply_filter">Filtri</string>
<string name="filter_apply">Apliki</string>
<string name="compose_shortcut_long_label">Verki mesaĝon</string>
<string name="compose_shortcut_short_label">Verki</string>
<string name="notification_clear_text">Ĉu vi certas ke vi volas proĉiame viŝi ĉiujn viajn sciigojn\?</string>
<string name="notification_clear_text">Ĉu vi certas, ke vi volas porĉiame viŝi ĉiujn viajn sciigojn\?</string>
<string name="compose_preview_image_description">Agoj por bildo %s</string>
<string name="poll_info_format"> <!-- 15 votes • 1 hour left --> %1$s • %2$s</string>
<plurals name="poll_info_votes">
@ -383,15 +383,15 @@
<item quantity="other">%s voĉdonoj</item>
</plurals>
<string name="poll_info_time_absolute">finiĝos je %s</string>
<string name="poll_info_closed">finiĝita</string>
<string name="poll_info_closed">finita</string>
<string name="poll_vote">Voĉdoni</string>
<string name="poll_ended_voted">Enketo al kiu vi voĉdonis finiĝis</string>
<string name="poll_ended_created">Enketo kiu vi kreis finiĝis</string>
<string name="poll_ended_voted">Enketo, al kiu vi voĉdonis, finiĝis</string>
<string name="poll_ended_created">Enketo, kiun vi kreis, finiĝis</string>
<string name="title_domain_mutes">Kaŝitaj domajnoj</string>
<string name="action_view_domain_mutes">Kaŝitaj domajnoj</string>
<string name="action_mute_domain">Silentigi %s</string>
<string name="confirmation_domain_unmuted">%s malsilentigita</string>
<string name="mute_domain_warning">Ĉu vi certas ke vi volas tute bloki %s\? Vi ne vidos enhavon de tiu domajno en publika tempolinio aŭ en viaj sciigoj. Viaj sekvantoj de tiu domajno estos forigitaj.</string>
<string name="mute_domain_warning">Ĉu vi certas ke vi volas tute bloki %s\? Vi ne vidos enhavon de tiu domajno en publika tempolinio aŭ en viaj sciigoj. Viaj sekvantoj el tiu domajno estos forigitaj.</string>
<string name="mute_domain_warning_dialog_ok">Kaŝi la tutan domajnon</string>
<string name="caption_notoemoji">La aktuala emoĝiaro de Google</string>
<string name="description_poll">Balotenketo kun elektoj: %1$s, %2$s, %3$s, %4$s, %5$s</string>
@ -405,14 +405,14 @@
<string name="failed_fetch_posts">Venigo de statusoj malsukcesis</string>
<string name="report_description_1">La signalo estos sendita al la kontrolantoj de via servilo. Vi povas doni klarigon pri kial vi signalas ĉi tiun konton sube:</string>
<string name="report_description_remote_instance">La konto estas en alia servilo. Ĉu sendi sennomigitan kopion de la signalo ankaŭ tien\?</string>
<string name="pref_title_show_notifications_filter">Montri filtrilon de Sciigoj</string>
<string name="pref_title_show_notifications_filter">Montri filtrilon de sciigoj</string>
<string name="filter_dialog_whole_word">Tuta vorto</string>
<string name="filter_dialog_whole_word_description">Kiam ĉefvorto aŭ frazo estas nur litercifera, tio aplikos nur se ĝi kongruas la tutan vorton</string>
<string name="filter_dialog_whole_word_description">Ŝlosilvorto aŭ frazo litercifera aplikiĝos, nur se ĝi kongruas kun la tuta vorto</string>
<string name="title_accounts">Kontoj</string>
<string name="failed_search">Serĉo malsukcesis</string>
<string name="action_add_poll">Aldoni baloton</string>
<string name="pref_title_alway_open_spoiler">Ĉiam pligrandigi tootoj markiĝita per enhavaj avertoj</string>
<string name="create_poll_title">Baloto</string>
<string name="pref_title_alway_open_spoiler">Ĉiam montri mesaĝojn kun enhavaj avertoj</string>
<string name="create_poll_title">Enketo</string>
<string name="duration_5_min">5 minutoj</string>
<string name="duration_30_min">30 minutoj</string>
<string name="duration_1_hour">1 horo</string>
@ -436,9 +436,9 @@
<string name="description_post_bookmarked">Aldonita al la legosignoj</string>
<string name="select_list_title">Elekti la liston</string>
<string name="list">Listo</string>
<string name="post_lookup_error_format">Eraro dum elserĉo de la mesaĝo %s</string>
<string name="no_drafts">Vi ne havas iun ajn malneton.</string>
<string name="no_scheduled_posts">Vi ne havas iun ajn planitan mesaĝon.</string>
<string name="post_lookup_error_format">Eraro dum serĉo de la mesaĝo %s</string>
<string name="no_drafts">Vi havas neniun malneton.</string>
<string name="no_scheduled_posts">Vi havas neniun planitan mesaĝon.</string>
<string name="notification_follow_request_name">Petoj de sekvado</string>
<string name="hashtags">Kradvortoj</string>
<plurals name="poll_info_people">
@ -449,8 +449,8 @@
<string name="notification_follow_request_description">Sciigoj pri petoj de sekvado</string>
<string name="pref_title_gradient_for_media">Montri buntajn transirojn por kaŝitaj aŭdovidaĵoj</string>
<string name="dialog_mute_hide_notifications">Kaŝi la sciigojn</string>
<string name="dialog_mute_warning">Silentigi @%s\?</string>
<string name="dialog_block_warning">Bloki @%s\?</string>
<string name="dialog_mute_warning">Ĉu silentigi @%s\?</string>
<string name="dialog_block_warning">Ĉu bloki @%s\?</string>
<string name="action_unmute_conversation">Malsilentigi la konversacion</string>
<string name="action_mute_conversation">Silentigi la konversacion</string>
<string name="action_unmute_domain">Malsilentigi %s</string>
@ -488,48 +488,48 @@
<string name="pref_title_enable_swipe_for_tabs">Ebligi ŝovumadon por ŝanĝi inter la langetoj</string>
<string name="warning_scheduling_interval">Mastodon havas minimuman intervalon de planado de 5 minutoj.</string>
<string name="post_media_attachments">Kunsendaĵoj</string>
<string name="pref_title_notification_filter_subscriptions">iu kiun mi sekvas afiŝis novan mesaĝon</string>
<string name="pref_title_notification_filter_subscriptions">iu, kiun mi sekvas, afiŝis novan mesaĝon</string>
<string name="dialog_delete_list_warning">Ĉu vi vere volas forigi la liston %s\?</string>
<string name="post_media_audio">Aŭdio</string>
<string name="post_media_audio">Aŭdo</string>
<string name="action_subscribe_account">Aboni</string>
<string name="draft_deleted">Malneto forigita</string>
<plurals name="error_upload_max_media_reached">
<item quantity="one">Vi ne povas elŝuti pli ol %1$d aŭdovidaĵa kunsendaĵo.</item>
<item quantity="other">Vi ne povas elŝuti pli ol %1$d aŭdovidaĵaj kunsendaĵoj.</item>
<item quantity="one">Vi ne povas elŝuti pli ol %1$d aŭdovida kunsendaĵo.</item>
<item quantity="other">Vi ne povas elŝuti pli ol %1$d aŭdovidaj kunsendaĵoj.</item>
</plurals>
<string name="label_duration">Daŭro</string>
<string name="duration_indefinite">Nedefinita</string>
<string name="action_unsubscribe_account">Malaboni</string>
<string name="notification_subscription_name">Novaj mesaĝoj</string>
<string name="action_unbookmark">Forigi la legosignon</string>
<string name="dialog_delete_conversation_warning">Ĉu forigi ĉi-tiun konversacion\?</string>
<string name="dialog_delete_conversation_warning">Ĉu forigi ĉi tiun konversacion\?</string>
<string name="pref_title_animate_custom_emojis">Animacii proprajn emoĝiojn</string>
<string name="wellbeing_hide_stats_profile">Kaŝi kvantecajn statistikaĵojn sur la profiloj</string>
<string name="wellbeing_hide_stats_profile">Kaŝi kvantecajn statistikaĵojn pri la profiloj</string>
<string name="action_delete_conversation">Forigi konversacion</string>
<string name="notification_subscription_format">%s ĵus afiŝis</string>
<string name="notification_subscription_description">Sciigoj kiam iu kiun vi sekvas afiŝis novan mesaĝon</string>
<string name="drafts_post_failed_to_send">Sendo de ĉi-tiu mesaĝo malsukcesis!</string>
<string name="wellbeing_hide_stats_posts">Kaŝi kvantecajn statistikaĵojn sur la mesaĝoj</string>
<string name="notification_subscription_description">Sciigoj, kiam iu, kiun vi sekvas, afiŝis novan mesaĝon</string>
<string name="drafts_post_failed_to_send">Sendo de ĉi tiu mesaĝo malsukcesis!</string>
<string name="wellbeing_hide_stats_posts">Kaŝi kvantecajn statistikaĵojn pri la mesaĝoj</string>
<string name="pref_title_confirm_favourites">Demandi konfirmon antaŭ ol stelumi</string>
<string name="pref_title_wellbeing_mode">Bonstato</string>
<string name="drafts_failed_loading_reply">Ŝarĝado de respondaj informoj malsukcesis</string>
<string name="wellbeing_mode_notice">Kelkaj informoj kiuj povas afekci vian mensan bonstaton estos kaŝitaj. Ĉi tiuj inkluzivas:
\n
\n - Sciigoj pri stelumo/diskonigo/sekvado
\n- Nombro de stelumoj/diskonigoj sur la mesaĝoj
\n- Statistikoj pri mesaĝoj/sekvantoj sur la profiloj
\n Sciigoj pri stelumo/diskonigo/sekvado
\n Nombro de stelumoj/diskonigoj sur la mesaĝoj
\n Statistikoj pri mesaĝoj/sekvantoj sur la profiloj
\n
\n Puŝosciigoj ne estos influitaj, sed vi povas kontroli viajn sciigojn preferojn permane.</string>
\n Sciigoj ne estos influitaj, sed vi povas kontroli viajn agordojn pri sciigojn permane.</string>
<string name="review_notifications">Kontroli la sciigojn</string>
<string name="limit_notifications">Limigi sciigojn pri tempolinio</string>
<string name="drafts_post_reply_removed">La mesaĝo al kiu ĉi tiu malneto respondas estis forigita</string>
<string name="drafts_post_reply_removed">La mesaĝo, al kiu tiu ĉi malneto respondas, estis forigita</string>
<string name="notification_sign_up_format">%s registriĝis</string>
<string name="pref_title_notification_filter_sign_ups">iu registriĝis</string>
<string name="pref_title_notification_filter_updates">mesaĝo kun kiu mi interagis estas redaktita</string>
<string name="pref_title_notification_filter_updates">mesaĝo, kun kiu mi interagis, estas redaktita</string>
<string name="notification_sign_up_name">Novaj kontoj</string>
<string name="notification_sign_up_description">Sciigoj pri novaj uzantoj</string>
<string name="status_count_one_plus">1+</string>
<string name="follow_requests_info">Kvankam via konto ne estas blokita, la teamo de %1$s pensas ke vi eble volus permane validigi la sekvopetojn de tiuj ĉi kontoj.</string>
<string name="follow_requests_info">Kvankam via konto ne estas ŝlosita, la teamo de %1$s pensas, ke vi eble volus permane validigi la sekvopetojn de tiuj ĉi kontoj.</string>
<string name="error_multimedia_size_limit">Filmetoj kaj sondosieroj ne povas esti pli grandaj ol %s MB.</string>
<string name="error_image_edit_failed">La bildo ne povis esti redaktita.</string>
<string name="title_login">Ensaluti</string>
@ -540,7 +540,7 @@
<string name="action_dismiss">Fermi</string>
<string name="action_details">Detaloj</string>
<string name="notification_update_name">Redaktitaj mesaĝoj</string>
<string name="notification_update_description">Sciigoj kiam mesaĝoj kun kiuj vi interagis estas redaktitaj</string>
<string name="notification_update_description">Sciigoj, kiam mesaĝoj, kun kiuj vi interagis, estas redaktitaj</string>
<string name="action_edit_image">Redakti la bildon</string>
<string name="duration_14_days">14 tagoj</string>
<string name="duration_30_days">30 tagoj</string>
@ -550,8 +550,21 @@
<string name="duration_365_days">365 tagoj</string>
<string name="tusky_compose_post_quicksetting_label">Ekverki mesaĝon</string>
<string name="account_date_joined">Aliĝis je %1$s</string>
<string name="saving_draft">Registras la malneton</string>
<string name="saving_draft">Konservado de la malneto</string>
<string name="tips_push_notification_migration">Ensalutu denove al ĉiuj kontoj por ŝalti sciigojn.</string>
<string name="error_could_not_load_login_page">La salutpaĝo ne povis esti ŝargita.</string>
<string name="error_loading_account_details">Ŝargo de detaloj pri la konto malsukcesis</string>
<string name="delete_scheduled_post_warning">Ĉu forigi tiun planitan mesaĝon\?</string>
<string name="instance_rule_info">Si vi ensalutas, vi konsentas je la regulo de %s.</string>
<string name="instance_rule_title">Regulo de %s</string>
<string name="duration_no_change">(Neniu ŝanĝo)</string>
<string name="filter_expiration_format">%s (%s)</string>
<string name="pref_show_self_username_always">Ĉiam</string>
<string name="pref_show_self_username_disambiguate">Kiam vi uzas plurajn kontojn</string>
<string name="pref_show_self_username_never">Neniam</string>
<string name="pref_title_show_self_username">Montri uzantnomon en ilobreto</string>
<string name="description_post_language">Mesaĝolingvo</string>
<string name="dialog_push_notification_migration">Por ricevi sciigoj per UnifiedPush, Tusky bezonas taŭgan permeson el Mastodon-servilo. Tio postulas re-ensaluton por ŝanĝi OAuth-rajtoj donitaj al Tusky. Se vi uzas la opcion re-ensaluti ĉi tie aŭ en la agordoj de la konto, viaj malnetoj kaj kaŝmemoroj estos konservitaj.</string>
<string name="dialog_push_notification_migration_other_accounts">Vi re-ensalutis en tiu konto por doni sciigo-permeson al Tusky. Vi havas tamen aliajn kontojn, ĉe kiuj vi devas re-sensaluti. Iru al ili, kaj re-ensalutu por ebligi ricevon de sciigoj per UnifiedPush.</string>
<string name="url_domain_notifier">%s (🔗 %s)</string>
</resources>

View File

@ -9,12 +9,12 @@
<string name="error_authorization_unknown">Ocurrió un error de autorización no identificado.</string>
<string name="error_authorization_denied">La autorización falló.</string>
<string name="error_retrieving_oauth_token">Fallo al obtener identificador de login.</string>
<string name="error_compose_character_limit">¡El estado es demasiado largo!</string>
<string name="error_compose_character_limit">¡La publicación es demasiado larga!</string>
<string name="error_media_upload_type">No se admite este tipo de archivo.</string>
<string name="error_media_upload_opening">No pudo abrirse el fichero.</string>
<string name="error_media_upload_permission">Se requiere permiso para acceder al almacenamiento.</string>
<string name="error_media_download_permission">Se requiere permiso para descargar al almacenamiento.</string>
<string name="error_media_upload_image_or_video">No se pueden adjuntar imágenes y vídeos en el mismo estado.</string>
<string name="error_media_upload_image_or_video">No se pueden adjuntar imágenes y vídeos en la misma publicación.</string>
<string name="error_media_upload_sending">La subida falló.</string>
<string name="error_sender_account_gone">Error al publicar.</string>
<string name="title_home">Inicio</string>
@ -23,7 +23,7 @@
<string name="title_public_federated">Federada</string>
<string name="title_direct_messages">Mensajes Directos</string>
<string name="title_tab_preferences">Pestañas</string>
<string name="title_view_thread">Publicación</string>
<string name="title_view_thread">Hilo</string>
<string name="title_posts">Estados</string>
<string name="title_posts_with_replies">Con respuestas</string>
<string name="title_posts_pinned">Fijado</string>
@ -47,8 +47,8 @@
<string name="post_content_show_less">Ocultar</string>
<string name="message_empty">Nada aquí.</string>
<string name="footer_empty">Nada por aquí. ¡Arrastra hacia abajo para recargar!</string>
<string name="notification_reblog_format">%s impulsó tu toot</string>
<string name="notification_favourite_format">%s marcó favorito</string>
<string name="notification_reblog_format">%s impulsó tu publicación</string>
<string name="notification_favourite_format">%s marcó como favorita tu publicación</string>
<string name="notification_follow_format">%s te siguió</string>
<string name="report_username_format">Reportar @%s</string>
<string name="report_comment_hint">¿Información adicional?</string>
@ -98,7 +98,7 @@
<string name="action_reject">Rechazar</string>
<string name="action_search">Buscar</string>
<string name="action_access_drafts">Borradores</string>
<string name="action_toggle_visibility">Visibilidad del estado</string>
<string name="action_toggle_visibility">Visibilidad de la publicación</string>
<string name="action_content_warning">Aviso de contenido</string>
<string name="action_emoji_keyboard">Teclado de emojis</string>
<string name="action_add_tab">Añadir pestaña</string>
@ -194,15 +194,16 @@
<string name="notification_follow_name">Nuevos seguidores</string>
<string name="notification_follow_description">Notificaciones de nuevos seguidores</string>
<string name="notification_boost_name">Impulsos</string>
<string name="notification_boost_description">Notificaciones de estados que fueron compartidos</string>
<string name="notification_boost_description">Notificaciones cuando impulsan tus publicaciones</string>
<string name="notification_favourite_name">Favoritos</string>
<string name="notification_favourite_description">Notificaciones de estados que recibieron favorito</string>
<string name="notification_favourite_description">Notificaciones de tus estados marcados como favorito</string>
<string name="notification_mention_format">%s te mencionó</string>
<string name="notification_summary_large">%1$s, %2$s, %3$s y %4$d otros</string>
<string name="notification_summary_medium">%1$s, %2$s, y %3$s</string>
<string name="notification_summary_small">%1$s y %2$s</string>
<plurals name="notification_title_summary">
<item quantity="one">%d nueva interacción</item>
<item quantity="many">%d nuevas interacciones</item>
<item quantity="other">%d nuevas interacciones</item>
</plurals>
<string name="description_account_locked">Cuenta protegida</string>
@ -230,6 +231,7 @@
<string name="post_media_video">Video</string>
<string name="state_follow_requested">Solicitud enviada</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="status_created_at_now">Ahora</string>
<string name="abbreviated_in_years">en %dy</string>
<string name="abbreviated_in_days">en %dd</string>
<string name="abbreviated_in_hours">en %dh</string>
@ -249,10 +251,15 @@
<string name="add_account_description">Añadir cuenta de Mastodon</string>
<string name="action_lists">Listas</string>
<string name="title_lists">Listas</string>
<string name="compose_active_account_description">Publicando con la cuenta %1$s</string>
<string name="compose_active_account_description">Publicar como %1$s</string>
<string name="error_failed_set_caption">Error al añadir leyenda</string>
<plurals name="hint_describe_for_visually_impaired">
<item quantity="other">Describir para invidentes\n(límite de %d caracteres)</item>
<item quantity="one">Descripción para personas con problemas de visión
\n(Límite de %d caracter)</item>
<item quantity="many">Descripción para personas con problemas de visión
\n(Límite de %d caracteres)</item>
<item quantity="other">Descripción para personas con problemas de visión
\n(Límite de %d caracteres)</item>
</plurals>
<string name="action_set_caption">Añadir leyenda</string>
<string name="action_remove">Eliminar</string>
@ -297,12 +304,14 @@
<string name="unpin_action">No fijar</string>
<string name="pin_action">Fijar</string>
<plurals name="favs">
<item quantity="one">&lt;b&gt;%1$s&lt;/b&gt; Favorito</item>
<item quantity="other">&lt;b&gt;%1$s&lt;/b&gt; Favoritos</item>
<item quantity="one"><b>%1$s</b> Favorito</item>
<item quantity="many"><b>%1$s</b> Favoritos</item>
<item quantity="other"><b>%1$s</b> Favoritos</item>
</plurals>
<plurals name="reblogs">
<item quantity="one"><b>%s</b> impulso</item>
<item quantity="other"><b>%s</b> impulsos</item>
<item quantity="one"><b>%s</b> Impulso</item>
<item quantity="many"><b>%s</b> Impulsos</item>
<item quantity="other"><b>%s</b> Impulsos</item>
</plurals>
<string name="title_reblogged_by">Impulsado por</string>
<string name="title_favourited_by">Marcado como favorito por</string>
@ -311,6 +320,7 @@
<string name="conversation_more_recipients">%1$s, %2$s y %3$d más</string>
<plurals name="max_tab_number_reached">
<item quantity="one">máximo de %1$d pestaña alcanzada</item>
<item quantity="many">máximo de %1$d pestañas alcanzadas</item>
<item quantity="other">máximo de %1$d pestañas alcanzadas</item>
</plurals>
<string name="action_mentions">Menciones</string>
@ -335,23 +345,28 @@
<string name="poll_vote">Votar</string>
<plurals name="poll_timespan_days">
<item quantity="one">%d día restante</item>
<item quantity="many">%d días restantes</item>
<item quantity="other">%d días restante</item>
</plurals>
<plurals name="poll_timespan_hours">
<item quantity="one">%d hora restante</item>
<item quantity="other">%d horas restante</item>
<item quantity="many">%d horas restantes</item>
<item quantity="other">%d horas restantes</item>
</plurals>
<plurals name="poll_timespan_minutes">
<item quantity="one">%d minuto restante</item>
<item quantity="other">%d minutos restante</item>
<item quantity="many">%d minutos restantes</item>
<item quantity="other">%d minutos restantes</item>
</plurals>
<plurals name="poll_timespan_seconds">
<item quantity="one">%d segundo restante</item>
<item quantity="other">%d segundos restante</item>
<item quantity="many">%d segundos restantes</item>
<item quantity="other">%d segundos restantes</item>
</plurals>
<string name="poll_info_format"> <!-- 15 votos • queda 1 hora --> %1$s • %2$s</string>
<plurals name="poll_info_votes">
<item quantity="one">%s voto</item>
<item quantity="many">%s votos</item>
<item quantity="other">%s votos</item>
</plurals>
<string name="poll_info_closed">cerrada</string>
@ -388,7 +403,7 @@
<string name="edit_hashtag_hint">Etiqueta sin #</string>
<string name="notifications_clear">Limpiar</string>
<string name="notifications_apply_filter">Filtro</string>
<string name="compose_shortcut_long_label">Componer toot</string>
<string name="compose_shortcut_long_label">Escribir publicación</string>
<string name="compose_shortcut_short_label">Redactar</string>
<string name="notification_clear_text">¿Estás seguro de que quieres eliminar permanentemente todas tus notificaciones\?</string>
<string name="compose_preview_image_description">Acciones para la imagen %s</string>
@ -419,7 +434,7 @@
<string name="report_description_1">El reporte será enviado a un moderador de tu servidor. Puedes añadir una explicación de por qué estás reportando esta cuenta a continuación:</string>
<string name="report_description_remote_instance">La cuenta es de otro servidor. ¿Enviar una copia anónima del reporte\?</string>
<string name="pref_title_show_notifications_filter">Mostrar filtro de notificaciones</string>
<string name="pref_title_alway_open_spoiler">Mostrar siempre toots marcados con avisos de contenido</string>
<string name="pref_title_alway_open_spoiler">Mostrar siempre publicaciones marcadas con avisos de contenido</string>
<string name="title_accounts">Cuentas</string>
<string name="failed_search">Error al buscar</string>
<string name="action_add_poll">Añadir encuesta</string>
@ -463,6 +478,7 @@
<string name="pref_title_enable_swipe_for_tabs">Habilitar gesto de deslizar para alternar entre pestañas</string>
<plurals name="poll_info_people">
<item quantity="one">%s persona</item>
<item quantity="many">%s personas</item>
<item quantity="other">%s personas</item>
</plurals>
<string name="hashtags">Etiquetas</string>
@ -485,20 +501,21 @@
<string name="notification_subscription_format">%s recién publicado</string>
<plurals name="error_upload_max_media_reached">
<item quantity="one">No puedes cargar más de %1$d archivo multimedia adjunto.</item>
<item quantity="many">No puedes cargar más de %1$d archivos multimedia adjuntos.</item>
<item quantity="other">No puedes cargar más de %1$d archivos multimedia adjuntos.</item>
</plurals>
<string name="wellbeing_hide_stats_profile">Esconder las estadísticas cuantitativas de los perfiles</string>
<string name="wellbeing_hide_stats_posts">Esconder las estadísticas cuantitativas de las publicaciones</string>
<string name="review_notifications">Revisar Notificaciones</string>
<string name="pref_title_wellbeing_mode">Bienestar</string>
<string name="notification_subscription_description">Notificaciones cuando alguien al que estoy suscrito publicó un nuevo toot</string>
<string name="notification_subscription_name">Nuevos toots</string>
<string name="pref_title_notification_filter_subscriptions">alguien al que estoy suscrito publicó un nuevo toot</string>
<string name="wellbeing_mode_notice">Algunas informaciones que podríam afectar tu bienestar van a ser ocultas. Esto incluye:
<string name="notification_subscription_description">Notificaciones cuando alguien al que estoy suscrito escribe una publicación</string>
<string name="notification_subscription_name">Nuevas publicaciones</string>
<string name="pref_title_notification_filter_subscriptions">alguien al que estoy suscrito hizo una nueva publicación</string>
<string name="wellbeing_mode_notice">Se ocultarán algunas informaciones que podrían afectar a tu bienestar. Esto incluye:
\n
\n- Notificaciones de favoritos, impulsos y seguidores
\n- Conteo de favoritos e impulsos en toots
\n- Estadísticas de seguidores e toots en perfiles
\n- Conteo de favoritos e impulsos en publicaciones
\n- Estadísticas de seguidores y publicaciones en perfiles
\n
\nLas notificaciones Push no serán afectadas, pero puedes revisar manualmente tus preferencias.</string>
<string name="drafts_post_reply_removed">El toot al que redactaste una respuesta ha sido eliminado</string>
@ -519,4 +536,54 @@
<string name="action_unsubscribe_account">Darse de baja</string>
<string name="action_delete_conversation">Eliminar conversación</string>
<string name="pref_title_confirm_favourites">Mostrar diálogo de confirmación antes de marcar como favorito</string>
<string name="pref_title_notification_filter_updates">una publicación con la que interactué se editó</string>
<string name="error_multimedia_size_limit">Los archivos de video y audio no pueden pesar más de %s MB.</string>
<string name="error_image_edit_failed">La imagen no pudo ser editada.</string>
<string name="pref_show_self_username_always">Siempre</string>
<string name="pref_show_self_username_never">Nunca</string>
<string name="action_add_reaction">añadir reacción</string>
<string name="pref_title_notification_filter_sign_ups">alguien se registró</string>
<string name="error_following_hashtag_format">Error al seguir #%s</string>
<string name="error_unfollowing_hashtag_format">Error dejando de seguir #%s</string>
<string name="title_login">Ingreso</string>
<string name="title_migration_relogin">Reingresa para activar notificaciones push</string>
<string name="notification_sign_up_format">%s se registró</string>
<string name="notification_update_format">%s editó su publicación</string>
<string name="action_dismiss">Descartar</string>
<string name="action_details">Detalles</string>
<string name="error_loading_account_details">Fallo cargando los detalles de la cuenta</string>
<string name="error_could_not_load_login_page">Fallo cargando la página de ingreso.</string>
<string name="delete_scheduled_post_warning">¿Eliminar publicación programada\?</string>
<string name="set_focus_description">Toca o arrastra el círculo para centrar el foco de la imagen, que será visible en las miniaturas.</string>
<string name="compose_save_draft_loses_media">¿Guardar este borrador\? (Los adjuntos se subirán de nuevo cuando vuelvas a él.)</string>
<string name="pref_title_show_self_username">Mostrar nombre de usuario en la barra de herramientas</string>
<string name="account_date_joined">Se unió %1$s</string>
<string name="duration_14_days">14 días</string>
<string name="duration_365_days">365 días</string>
<string name="tips_push_notification_migration">Inicia sesión de nuevo en todas las cuentas para activar las notificaciones push.</string>
<string name="dialog_push_notification_migration">Para poder usar las notificaciones push con UnifiedPush, Tusky necesita permiso para suscribirse a las notificaciones de tu servidor de Mastodon. Es necesario volver a acceder para cambiar los parámetros OAuth concedidos a Tusky. Usar aquí, o en las Preferencias de la Cuenta, la opción de volver a acceder conservarás los borradores y la caché.</string>
<string name="failed_to_pin">Fallo al fijar</string>
<string name="failed_to_unpin">Fallo al quitarlo</string>
<string name="pref_show_self_username_disambiguate">Cuando hay varias cuentas activas</string>
<string name="url_domain_notifier">%s (🔗 %s)</string>
<string name="notification_sign_up_description">Notificaciones de nuevos usuarios</string>
<string name="notification_update_name">Ediciones de una publicación</string>
<string name="notification_update_description">Notificaciones cuando se editan publicaciones con las que has interactuado</string>
<string name="status_count_one_plus">1+</string>
<string name="filter_expiration_format">%s (%s)</string>
<string name="error_failed_set_focus">Fallo al establecer foco</string>
<string name="action_set_focus">Establece el foco</string>
<string name="description_post_language">Idioma de publicación</string>
<string name="duration_30_days">30 días</string>
<string name="duration_60_days">60 días</string>
<string name="duration_90_days">90 días</string>
<string name="duration_180_days">180 días</string>
<string name="duration_no_change">(Sin cambios)</string>
<string name="tusky_compose_post_quicksetting_label">Escribir publicación</string>
<string name="saving_draft">Guardando borrador…</string>
<string name="dialog_push_notification_migration_other_accounts">Has vuelto a iniciar sesión en esta cuenta para dar permiso de notificaciones push a Tusky. Sin embargo, aún hay otras cuentas que no tienen este permiso. Cambia a estas cuentas y vuelve a iniciar sesión, una a una, para activar el soporte de notificaciones de UnifiedPush.</string>
<string name="instance_rule_info">Al iniciar sesión aceptas las normas de %s.</string>
<string name="instance_rule_title">Normas de %s</string>
<string name="notification_sign_up_name">Creación de cuentas</string>
<string name="action_edit_image">Editar imagen</string>
</resources>

View File

@ -227,9 +227,11 @@
<string name="add_account_description">افزودن حساب ماستودون جدید</string>
<string name="action_lists">فهرست‌ها</string>
<string name="title_lists">فهرست‌ها</string>
<string name="compose_active_account_description">در حال فرستادن با حساب %1$s</string>
<string name="compose_active_account_description">فرستادن از طرف %1$s</string>
<string name="error_failed_set_caption">شکست در تنظیم عنوان</string>
<plurals name="hint_describe_for_visually_impaired">
<item quantity="one">توصیف برای کم‌بینایان
\n(کران ۱ نویسه)</item>
<item quantity="other">توصیف برای کم‌بینایان
\n(کران %d نویسه)</item>
</plurals>
@ -340,7 +342,7 @@
<string name="license_cc_by_4">نگارش ۴٫۰ CC-BY</string>
<string name="license_cc_by_sa_4">نگارش ۴٫۰ CC-BY-SA</string>
<plurals name="favs">
<item quantity="one"><b>%1$s</b> برگزیدن</item>
<item quantity="one">۱ برگزیدن</item>
<item quantity="other"><b>%1$s</b> برگزیدن</item>
</plurals>
<plurals name="reblogs">
@ -541,4 +543,27 @@
<string name="action_dismiss">رد کردن</string>
<string name="action_details">جزییات</string>
<string name="saving_draft">ذخیرهٔ پیش‌نویس…</string>
<string name="error_following_hashtag_format">خطا در پی‌گیری #%s</string>
<string name="error_unfollowing_hashtag_format">خطا در ناپی‌گیری #%s</string>
<string name="delete_scheduled_post_warning">حذف این فرستهٔ زمان‌بسته؟</string>
<string name="instance_rule_title">قواعد %s</string>
<string name="instance_rule_info">با ورودتان، قواعد %s را می‌پذیرید.</string>
<string name="filter_expiration_format">%s (%s)</string>
<string name="failed_to_pin">شکست در سنجاق کردن</string>
<string name="failed_to_unpin">شکست در برداشتن سنجاق</string>
<string name="error_multimedia_size_limit">پرونده‌های صوتی و ویدیویی نمی‌توانند بیش از %sمب باشند.</string>
<string name="error_image_edit_failed">تصویر نتوانست ویرایش شود.</string>
<string name="description_post_language">زبان فرسته</string>
<string name="pref_show_self_username_always">همیشه</string>
<string name="pref_show_self_username_disambiguate">هنگام ورود چندین حساب</string>
<string name="pref_show_self_username_never">هرگز</string>
<string name="pref_title_show_self_username">نمایش نام کاربری در نوارابزارها</string>
<string name="url_domain_notifier">%s (🔗 %s)</string>
<string name="action_add_reaction">افزودن واکنش</string>
<string name="error_failed_set_focus">شکست در تنظیم نقطهٔ تمرکز</string>
<string name="action_set_focus">تنظیم نقطهٔ تمرکز</string>
<string name="duration_no_change">(بدون تغییر)</string>
<string name="error_loading_account_details">شکست در بار کردن جزییات حساب</string>
<string name="set_focus_description">ضربه زده یا دایره را کشیده تا نقطهٔ کانونی‌ای که همواره باید در بندانگشتی‌ها نمایان باشد را برگزینید.</string>
<string name="compose_save_draft_loses_media">ذخیرهٔ پیش‌نویس؟ (پیوست‌ها هنگام بازگردانی پیش‌نویس، دوباره بارگذاری خواهند شد)</string>
</resources>

View File

@ -184,7 +184,7 @@
<string name="error_media_upload_type">Tällaista tiedostoa ei voida ladata ylös.</string>
<string name="notification_follow_request_format">%s haluaa seurata sinua</string>
<string name="title_public_federated">Verkostoitu</string>
<string name="error_media_upload_sending">Lähetys epäonnistui.</string>
<string name="error_media_upload_sending">Lähettäminen epäonnistui.</string>
<string name="report_username_format">Ilmianna @%s</string>
<string name="report_comment_hint">Lisähuomautuksia\?</string>
<string name="error_loading_account_details">Tilitietojen lataaminen epäonnistui</string>
@ -203,7 +203,7 @@
<string name="download_image">Ladataan kuvaa %1$s</string>
<string name="error_failed_set_caption">Kuvauksen lisääminen epäonnistui</string>
<string name="action_mute_conversation">Mykistä keskustelu</string>
<string name="error_generic">On syntynyt virhe.</string>
<string name="error_generic">Tapahtui virhe.</string>
<string name="action_logout_confirm">Halutako varmasti kirjautua ulos tililtä %s1\?</string>
<string name="action_unreblog">Poista jako</string>
<string name="action_create_list">Luo lista</string>
@ -218,7 +218,7 @@
<plurals name="hint_describe_for_visually_impaired">
<item quantity="one">Kuvaa näkövammaisille
\n(enintään %d merkkiä)</item>
<item quantity="other"></item>
<item quantity="other"/>
</plurals>
<string name="edit_hashtag_hint">Aihetunniste ilman #-merkkiä</string>
<string name="title_domain_mutes">Piilotetus verkkonimet</string>
@ -230,9 +230,9 @@
<string name="action_quick_reply">Vastaa nopeasti</string>
<string name="action_hide_reblogs">Piilota jaetut julkaisut</string>
<string name="footer_empty">Täällä ei ole mitään. Liu\'uta alaspäin päivittääksesi!</string>
<string name="error_network">On syntynyt verkostovirhe! Tarkista yhteytesi ja yritä uudelleen!</string>
<string name="error_network">Verkkovirhe! Tarkista yhteytesi ja yritä uudelleen!</string>
<string name="action_open_media_n">Avaa media No. %d</string>
<string name="error_sender_account_gone">Julkaisun lähetys epäonnistui.</string>
<string name="error_sender_account_gone">Julkaisun lähettäminen epäonnistui.</string>
<string name="error_empty">Tätä kenttää ei voi jättää tyhjäksi.</string>
<string name="title_announcements">Tiedotukset</string>
<string name="notification_subscription_description">Ilmoitukset seuraamiesi uusista julkaisuista</string>
@ -268,4 +268,47 @@
<string name="action_mute_notifications_desc">Mykistä ilmoitukset tililtä %s</string>
<string name="action_details">Yksityiskohdat</string>
<string name="error_image_edit_failed">Kuvaa ei voitu muokata.</string>
<string name="description_visibility_unlisted">Listaamaton</string>
<string name="dialog_delete_conversation_warning">Poista tämä keskustelu\?</string>
<string name="pref_title_show_media_preview">Lataa median esikatselu</string>
<string name="post_privacy_unlisted">Listaamaton</string>
<string name="pref_title_alway_show_sensitive_media">Näytä aina arkaluonteinen sisältö</string>
<string name="send_post_notification_cancel_title">Lähettäminen peruutettu</string>
<string name="description_visibility_private">Seuraajat</string>
<string name="pref_show_self_username_always">Aina</string>
<string name="pref_show_self_username_never">Ei koskaan</string>
<string name="notification_boost_name">Buustaukset</string>
<string name="notification_favourite_name">Suosikit</string>
<string name="notification_poll_name">Äänestykset</string>
<string name="notification_poll_description">Ilmoitukset päättyneistä äänestyksistä</string>
<string name="filter_edit_dialog_title">Muokkaa suodatinta</string>
<string name="action_add_reaction">lisää reaktio</string>
<string name="confirmation_reported">Lähetetty!</string>
<string name="post_sent">Lähetetty!</string>
<string name="post_sent_long">Vastaus lähetetty onnistuneesti.</string>
<string name="login_connection">Yhdistetään…</string>
<string name="dialog_mute_warning">Hiljennä @%s\?</string>
<string name="dialog_mute_hide_notifications">Piilota ilmoitukset</string>
<string name="pref_title_appearance_settings">Ulkoasu</string>
<string name="pref_title_show_boosts">Näytä buustaukset</string>
<string name="pref_default_media_sensitivity">Merkitse media aina arkaluontoiseksi</string>
<string name="pref_failed_to_sync">Asetusten synkronointi epäonnistui</string>
<string name="post_text_size_smallest">Pienin</string>
<string name="post_text_size_small">Pieni</string>
<string name="post_text_size_medium">Keskikokoinen</string>
<string name="post_text_size_large">Suuri</string>
<string name="post_text_size_largest">Suurin</string>
<string name="post_share_link">Jaa linkki postaukseen</string>
<string name="post_media_attachments">Liitteet</string>
<string name="title_media">Media</string>
<string name="load_more_placeholder_text">lataa lisää</string>
<string name="pref_title_public_filter_keywords">Julkiset aikajanat</string>
<string name="pref_title_thread_filter_keywords">Keskustelut</string>
<string name="filter_addition_dialog_title">Lisää suodatin</string>
<string name="later">Myöhemmin</string>
<string name="description_post_cw">Sisältövaroitus: %s</string>
<string name="pref_title_http_proxy_server">HTTP-välityspalvelin</string>
<string name="restart">Käynnistä uudelleen</string>
<string name="error_failed_app_registration">Tunnistautuminen valitsemasi instanssin kanssa epäonnistui.</string>
<string name="dialog_block_warning">Estä @%s\?</string>
</resources>

View File

@ -561,4 +561,6 @@
<string name="description_post_language">Langue du message</string>
<string name="duration_no_change">(Aucune modification)</string>
<string name="url_domain_notifier">%s (🔗 %s)</string>
<string name="action_add_reaction">ajouter une réaction</string>
<string name="instance_rule_title">%s règles</string>
</resources>

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