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

View File

@ -16,7 +16,6 @@
package com.keylesspalace.tusky; package com.keylesspalace.tusky;
import android.app.ActivityManager; import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
@ -92,11 +91,6 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
requesters = new HashMap<>(); requesters = new HashMap<>();
} }
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(TuskyApplication.getLocaleManager().setLocale(base));
}
protected boolean requiresLogin() { protected boolean requiresLogin() {
return true; 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.account.AccountActivity
import com.keylesspalace.tusky.components.viewthread.ViewThreadActivity import com.keylesspalace.tusky.components.viewthread.ViewThreadActivity
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.looksLikeMastodonUrl
import com.keylesspalace.tusky.util.openLink import com.keylesspalace.tusky.util.openLink
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import java.net.URI
import java.net.URISyntaxException
import javax.inject.Inject import javax.inject.Inject
/** this is the base class for all activities that open links /** 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 { enum class PostLookupFallbackBehavior {
OPEN_IN_BROWSER, OPEN_IN_BROWSER,
DISPLAY_ERROR, DISPLAY_ERROR,

View File

@ -434,9 +434,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
private fun setupDrawer(savedInstanceState: Bundle?, addSearchButton: Boolean) { private fun setupDrawer(savedInstanceState: Bundle?, addSearchButton: Boolean) {
binding.mainToolbar.setNavigationOnClickListener { val drawerOpenClickListener = View.OnClickListener { binding.mainDrawerLayout.open() }
binding.mainDrawerLayout.open()
} binding.mainToolbar.setNavigationOnClickListener(drawerOpenClickListener)
binding.topNavAvatar.setOnClickListener(drawerOpenClickListener)
binding.bottomNavAvatar.setOnClickListener(drawerOpenClickListener)
header = AccountHeaderView(this).apply { header = AccountHeaderView(this).apply {
headerBackgroundScaleType = ImageView.ScaleType.CENTER_CROP headerBackgroundScaleType = ImageView.ScaleType.CENTER_CROP
@ -648,7 +650,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
val actionBarSize = ThemeUtils.getDimension(this, R.attr.actionBarSize) val actionBarSize = ThemeUtils.getDimension(this, R.attr.actionBarSize)
val fabMargin = resources.getDimensionPixelSize(R.dimen.fabMargin) val fabMargin = resources.getDimensionPixelSize(R.dimen.fabMargin)
(binding.composeButton.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = actionBarSize + fabMargin (binding.composeButton.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = actionBarSize + fabMargin
binding.tabLayout.hide() binding.topNav.hide()
binding.bottomTabLayout binding.bottomTabLayout
} else { } else {
binding.bottomNav.hide() binding.bottomNav.hide()
@ -784,7 +786,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
val pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin) val pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin)
binding.viewPager.setPageTransformer(MarginPageTransformer(pageMargin)) 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 binding.viewPager.isUserInputEnabled = enableSwipeForTabs
onTabSelectedListener?.let { onTabSelectedListener?.let {
@ -922,71 +924,117 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
} }
private fun loadDrawerAvatar(avatarUrl: String, showPlaceholder: Boolean) { 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) val animateAvatars = preferences.getBoolean("animateGifAvatars", false)
if (animateAvatars) { if (hideTopToolbar) {
glide.asDrawable() val navOnBottom = preferences.getString("mainNavPosition", "top") == "bottom"
.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 onLoadStarted(placeholder: Drawable?) { val avatarView = if (navOnBottom) {
if (placeholder != null) { binding.bottomNavAvatar.show()
binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) binding.bottomNavAvatar
} } else {
} binding.topNavAvatar.show()
binding.topNavAvatar
}
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) { if (animateAvatars) {
if (resource is Animatable) { Glide.with(this)
resource.start() .load(avatarUrl)
} .placeholder(R.drawable.avatar_default)
binding.mainToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize) .into(avatarView)
} } else {
Glide.with(this)
override fun onLoadCleared(placeholder: Drawable?) { .asBitmap()
if (placeholder != null) { .load(avatarUrl)
binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) .placeholder(R.drawable.avatar_default)
} .into(avatarView)
} }
})
} else { } 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?) { binding.bottomNavAvatar.hide()
if (placeholder != null) { binding.topNavAvatar.hide()
binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
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>?) { override fun onLoadStarted(placeholder: Drawable?) {
binding.mainToolbar.navigationIcon = FixedSizeDrawable(BitmapDrawable(resources, resource), navIconSize, navIconSize) if (placeholder != null) {
} binding.mainToolbar.navigationIcon =
FixedSizeDrawable(placeholder, navIconSize, navIconSize)
}
}
override fun onLoadCleared(placeholder: Drawable?) { override fun onResourceReady(
if (placeholder != null) { resource: Drawable,
binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) 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.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import at.connyduck.calladapter.networkresult.fold 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.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.appstore.EventHub 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.TimelineFragment
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel.Kind import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel.Kind
import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
@ -52,13 +55,20 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
private val quickTootViewModel: QuickTootViewModel by viewModels { viewModelFactory } private val quickTootViewModel: QuickTootViewModel by viewModels { viewModelFactory }
private val binding: ActivityStatuslistBinding by viewBinding(ActivityStatuslistBinding::inflate) private val binding: ActivityStatuslistBinding by viewBinding(ActivityStatuslistBinding::inflate)
private lateinit var kind: Kind private lateinit var kind: Kind
private var hashtag: String? = null private var hashtag: String? = null
private var followTagItem: MenuItem? = null private var followTagItem: MenuItem? = null
private var unfollowTagItem: 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?) { override fun onCreate(savedInstanceState: Bundle?) {
Log.d("StatusListActivity", "onCreate")
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(binding.root) setContentView(binding.root)
@ -96,7 +106,7 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
eventHub.events eventHub.events
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.autoDispose(this, Lifecycle.Event.ON_DESTROY) .autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
.subscribe(binding.viewQuickToot::handleEvent) .subscribe(binding.viewQuickToot::handleEvent)
binding.floatingBtn.setOnClickListener(binding.viewQuickToot::onFABClicked) binding.floatingBtn.setOnClickListener(binding.viewQuickToot::onFABClicked)
} }
@ -110,10 +120,15 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
menuInflater.inflate(R.menu.view_hashtag_toolbar, menu) menuInflater.inflate(R.menu.view_hashtag_toolbar, menu)
followTagItem = menu.findItem(R.id.action_follow_hashtag) followTagItem = menu.findItem(R.id.action_follow_hashtag)
unfollowTagItem = menu.findItem(R.id.action_unfollow_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 followTagItem?.isVisible = tagEntity.following == false
unfollowTagItem?.isVisible = tagEntity.following == true unfollowTagItem?.isVisible = tagEntity.following == true
followTagItem?.setOnMenuItemClickListener { followTag() } followTagItem?.setOnMenuItemClickListener { followTag() }
unfollowTagItem?.setOnMenuItemClickListener { unfollowTag() } unfollowTagItem?.setOnMenuItemClickListener { unfollowTag() }
muteTagItem?.setOnMenuItemClickListener { muteTag() }
unmuteTagItem?.setOnMenuItemClickListener { unmuteTag() }
updateMuteTagMenuItems()
}, },
{ {
Log.w(TAG, "Failed to query tag #$tag", it) Log.w(TAG, "Failed to query tag #$tag", it)
@ -165,6 +180,85 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
return true 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 override fun androidInjector() = dispatchingAndroidInjector
companion object { companion object {

View File

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

View File

@ -16,8 +16,6 @@
package com.keylesspalace.tusky package com.keylesspalace.tusky
import android.app.Application import android.app.Application
import android.content.Context
import android.content.res.Configuration
import android.util.Log import android.util.Log
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.work.WorkManager import androidx.work.WorkManager
@ -44,6 +42,9 @@ class TuskyApplication : Application(), HasAndroidInjector {
@Inject @Inject
lateinit var notificationWorkerFactory: NotificationWorkerFactory lateinit var notificationWorkerFactory: NotificationWorkerFactory
@Inject
lateinit var localeManager: LocaleManager
override fun onCreate() { override fun onCreate() {
// Uncomment me to get StrictMode violation logs // Uncomment me to get StrictMode violation logs
// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { // 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) val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
ThemeUtils.setAppNightMode(theme) ThemeUtils.setAppNightMode(theme)
localeManager.setLocale()
RxJavaPlugins.setErrorHandler { RxJavaPlugins.setErrorHandler {
Log.w("RxJava", "undeliverable exception", it) 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 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.Bitmap
import android.graphics.Color import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.transition.Transition 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.databinding.ActivityViewMediaBinding
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.fragment.ViewImageFragment import com.keylesspalace.tusky.fragment.ViewImageFragment
import com.keylesspalace.tusky.fragment.ViewVideoFragment
import com.keylesspalace.tusky.pager.ImagePagerAdapter import com.keylesspalace.tusky.pager.ImagePagerAdapter
import com.keylesspalace.tusky.pager.SingleImagePagerAdapter import com.keylesspalace.tusky.pager.SingleImagePagerAdapter
import com.keylesspalace.tusky.util.getTemporaryMediaFilename import com.keylesspalace.tusky.util.getTemporaryMediaFilename
@ -67,7 +69,7 @@ import java.util.Locale
typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit
class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener { class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener, ViewVideoFragment.VideoActionsListener {
private val binding by viewBinding(ActivityViewMediaBinding::inflate) private val binding by viewBinding(ActivityViewMediaBinding::inflate)
@ -212,12 +214,20 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
} }
private fun requestDownloadMedia() { private fun requestDownloadMedia() {
requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { _, grantResults -> if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { _, grantResults ->
downloadMedia() if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
} else { downloadMedia()
showErrorDialog(binding.toolbar, R.string.error_media_download_permission, R.string.action_retry) { requestDownloadMedia() } } 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.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.TooltipCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.keylesspalace.tusky.databinding.ItemEmojiButtonBinding import com.keylesspalace.tusky.databinding.ItemEmojiButtonBinding
@ -52,6 +53,7 @@ class EmojiAdapter(
} }
emojiImageView.contentDescription = emoji.shortcode 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.ArrayAdapter
import android.widget.TextView import android.widget.TextView
import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.getTuskyDisplayName
import com.keylesspalace.tusky.util.modernLanguageCode
import java.util.Locale import java.util.Locale
class LocaleAdapter(context: Context, resource: Int, locales: List<Locale>) : ArrayAdapter<Locale>(context, resource, locales) { 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 { return (super.getView(position, convertView, parent) as TextView).apply {
setTextColor(ThemeUtils.getColor(context, android.R.attr.textColorTertiary)) setTextColor(ThemeUtils.getColor(context, android.R.attr.textColorTertiary))
typeface = Typeface.DEFAULT_BOLD 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 { override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
return (super.getDropDownView(position, convertView, parent) as TextView).apply { return (super.getDropDownView(position, convertView, parent) as TextView).apply {
setTextColor(ThemeUtils.getColor(context, android.R.attr.textColorTertiary)) setTextColor(ThemeUtils.getColor(context, android.R.attr.textColorTertiary))
val locale = super.getItem(position) text = super.getItem(position)?.getTuskyDisplayName(context)
text = "${locale?.displayLanguage} (${locale?.getDisplayLanguage(locale)})"
} }
} }
} }

View File

@ -43,6 +43,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide; import com.bumptech.glide.Glide;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding; import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding;
import com.keylesspalace.tusky.databinding.ItemReportNotificationBinding;
import com.keylesspalace.tusky.databinding.ViewQuoteInlineBinding; import com.keylesspalace.tusky.databinding.ViewQuoteInlineBinding;
import com.keylesspalace.tusky.entity.Emoji; import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.entity.Notification; 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 = 2;
private static final int VIEW_TYPE_FOLLOW_REQUEST = 3; private static final int VIEW_TYPE_FOLLOW_REQUEST = 3;
private static final int VIEW_TYPE_PLACEHOLDER = 4; 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[] COLLAPSE_INPUT_FILTER = new InputFilter[]{SmartLengthInputFilter.INSTANCE};
private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0]; 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); .inflate(R.layout.item_status_placeholder, parent, false);
return new PlaceholderViewHolder(view); return new PlaceholderViewHolder(view);
} }
case VIEW_TYPE_REPORT: {
ItemReportNotificationBinding binding = ItemReportNotificationBinding.inflate(inflater, parent, false);
return new ReportNotificationViewHolder(binding);
}
default: default:
case VIEW_TYPE_UNKNOWN: { case VIEW_TYPE_UNKNOWN: {
View view = new View(parent.getContext()); View view = new View(parent.getContext());
@ -256,6 +262,13 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
} }
break; 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: default:
} }
} }
@ -309,6 +322,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
case FOLLOW_REQUEST: { case FOLLOW_REQUEST: {
return VIEW_TYPE_FOLLOW_REQUEST; return VIEW_TYPE_FOLLOW_REQUEST;
} }
case REPORT: {
return VIEW_TYPE_REPORT;
}
default: { default: {
return VIEW_TYPE_UNKNOWN; return VIEW_TYPE_UNKNOWN;
} }
@ -327,6 +343,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
void onViewStatusForNotificationId(String notificationId); void onViewStatusForNotificationId(String notificationId);
void onViewReport(String reportId);
void onExpandedChange(boolean expanded, int position); 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; package com.keylesspalace.tusky.adapter;
import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription;
import android.content.Context; import android.content.Context;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.ColorDrawable;
@ -23,6 +25,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.core.text.HtmlCompat; import androidx.core.text.HtmlCompat;
import androidx.core.view.ViewKt;
import androidx.recyclerview.widget.DefaultItemAnimator; import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; 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.ThemeUtils;
import com.keylesspalace.tusky.util.TimestampUtils; import com.keylesspalace.tusky.util.TimestampUtils;
import com.keylesspalace.tusky.view.MediaPreviewImageView; import com.keylesspalace.tusky.view.MediaPreviewImageView;
import com.keylesspalace.tusky.view.MediaPreviewLayout;
import com.keylesspalace.tusky.viewdata.PollOptionViewData; import com.keylesspalace.tusky.viewdata.PollOptionViewData;
import com.keylesspalace.tusky.viewdata.PollViewData; import com.keylesspalace.tusky.viewdata.PollViewData;
import com.keylesspalace.tusky.viewdata.PollViewDataKt; import com.keylesspalace.tusky.viewdata.PollViewDataKt;
@ -67,14 +71,13 @@ import at.connyduck.sparkbutton.SparkButton;
import at.connyduck.sparkbutton.helpers.Utils; import at.connyduck.sparkbutton.helpers.Utils;
import kotlin.collections.CollectionsKt; import kotlin.collections.CollectionsKt;
import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription;
import net.accelf.yuito.QuoteInlineHelper; import net.accelf.yuito.QuoteInlineHelper;
public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
public static class Key { public static class Key {
public static final String KEY_CREATED = "created"; public static final String KEY_CREATED = "created";
} }
private TextView displayName; private TextView displayName;
private TextView username; private TextView username;
private ImageButton replyButton; private ImageButton replyButton;
@ -85,8 +88,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private SparkButton bookmarkButton; private SparkButton bookmarkButton;
private ImageButton moreButton; private ImageButton moreButton;
private ConstraintLayout mediaContainer; private ConstraintLayout mediaContainer;
protected MediaPreviewImageView[] mediaPreviews; protected MediaPreviewLayout mediaPreview;
private ImageView[] mediaOverlays;
private TextView sensitiveMediaWarning; private TextView sensitiveMediaWarning;
private View sensitiveMediaShow; private View sensitiveMediaShow;
protected TextView[] mediaLabels; protected TextView[] mediaLabels;
@ -139,19 +141,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
mediaContainer = itemView.findViewById(R.id.status_media_preview_container); mediaContainer = itemView.findViewById(R.id.status_media_preview_container);
mediaContainer.setClipToOutline(true); 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); sensitiveMediaWarning = itemView.findViewById(R.id.status_sensitive_media_warning);
sensitiveMediaShow = itemView.findViewById(R.id.status_sensitive_media_button); sensitiveMediaShow = itemView.findViewById(R.id.status_sensitive_media_button);
mediaLabels = new TextView[]{ mediaLabels = new TextView[]{
@ -190,8 +181,6 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
mediaPreviewUnloaded = new ColorDrawable(ThemeUtils.getColor(itemView.getContext(), R.attr.colorBackgroundAccent)); 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) { protected void setDisplayName(String name, List<Emoji> customEmojis, StatusDisplayOptions statusDisplayOptions) {
CharSequence emojifiedName = CustomEmojiHelper.emojify( CharSequence emojifiedName = CustomEmojiHelper.emojify(
name, customEmojis, displayName, statusDisplayOptions.animateEmojis() 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()) { if (statusDisplayOptions.useAbsoluteTime()) {
timestampInfo.setText(absoluteTimeFormatter.format(createdAt, true)); timestampText = absoluteTimeFormatter.format(createdAt, true);
} else { } else {
if (createdAt == null) { if (createdAt == null) {
timestampInfo.setText("?m"); timestampText = "?m";
} else { } else {
long then = createdAt.getTime(); long then = createdAt.getTime();
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
String readout = TimestampUtils.getRelativeTimeSpanString(timestampInfo.getContext(), then, now); 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, private CharSequence getCreatedAtDescription(Date createdAt,
@ -501,64 +496,51 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
Drawable placeholder = blurhash != null ? decodeBlurHash(blurhash) : mediaPreviewUnloaded; Drawable placeholder = blurhash != null ? decodeBlurHash(blurhash) : mediaPreviewUnloaded;
if (TextUtils.isEmpty(previewUrl)) { ViewKt.doOnLayout(imageView, view -> {
imageView.removeFocalPoint(); if (TextUtils.isEmpty(previewUrl)) {
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 {
imageView.removeFocalPoint(); imageView.removeFocalPoint();
Glide.with(imageView) Glide.with(imageView)
.load(previewUrl) .load(placeholder)
.placeholder(placeholder)
.centerInside() .centerInside()
.into(imageView); .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, protected void setMediaPreviews(final List<Attachment> attachments, boolean sensitive,
final StatusActionListener listener, boolean showingContent, final StatusActionListener listener, boolean showingContent,
boolean useBlurhash) { 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); mediaPreview.forEachIndexed((i, imageView) -> {
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++) {
Attachment attachment = attachments.get(i); Attachment attachment = attachments.get(i);
String previewUrl = attachment.getPreviewUrl(); String previewUrl = attachment.getPreviewUrl();
String description = attachment.getDescription(); String description = attachment.getDescription();
MediaPreviewImageView imageView = mediaPreviews[i];
imageView.setVisibility(View.VISIBLE);
if (TextUtils.isEmpty(description)) { if (TextUtils.isEmpty(description)) {
imageView.setContentDescription(imageView.getContext() imageView.setContentDescription(imageView.getContext()
@ -576,42 +558,38 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
final Attachment.Type type = attachment.getType(); final Attachment.Type type = attachment.getType();
if (showingContent && (type == Attachment.Type.VIDEO || type == Attachment.Type.GIFV)) { 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 { } else {
mediaOverlays[i].setVisibility(View.GONE); imageView.setForeground(null);
} }
setAttachmentClickListener(imageView, listener, i, attachment, true); setAttachmentClickListener(imageView, listener, i, attachment, true);
}
if (sensitive) { if (sensitive) {
sensitiveMediaWarning.setText(R.string.post_sensitive_media_title); sensitiveMediaWarning.setText(R.string.post_sensitive_media_title);
} else { } else {
sensitiveMediaWarning.setText(R.string.post_media_hidden_title); 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());
} }
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. return null;
for (int i = n; i < Status.MAX_MEDIA_ATTACHMENTS; i++) { });
mediaPreviews[i].setVisibility(View.GONE);
}
} }
@DrawableRes @DrawableRes
@ -835,7 +813,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
Status actionable = status.getActionable(); Status actionable = status.getActionable();
setDisplayName(actionable.getAccount().getName(), actionable.getAccount().getEmojis(), statusDisplayOptions); setDisplayName(actionable.getAccount().getName(), actionable.getAccount().getEmojis(), statusDisplayOptions);
setUsername(status.getUsername()); setUsername(status.getUsername());
setCreatedAt(actionable.getCreatedAt(), statusDisplayOptions); setCreatedAt(actionable.getCreatedAt(), actionable.getEditedAt(), statusDisplayOptions);
setStatusVisibility(actionable.getVisibility()); setStatusVisibility(actionable.getVisibility());
setIsReply(actionable.getInReplyToId() != null); setIsReply(actionable.getInReplyToId() != null);
setReplyCount(actionable.getRepliesCount()); setReplyCount(actionable.getRepliesCount());
@ -860,10 +838,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
} else { } else {
setMediaLabel(attachments, sensitive, listener, status.isShowingContent()); setMediaLabel(attachments, sensitive, listener, status.isShowingContent());
// Hide all unused views. // Hide all unused views.
mediaPreviews[0].setVisibility(View.GONE); mediaPreview.setVisibility(View.GONE);
mediaPreviews[1].setVisibility(View.GONE);
mediaPreviews[2].setVisibility(View.GONE);
mediaPreviews[3].setVisibility(View.GONE);
hideSensitiveMediaWarning(); hideSensitiveMediaWarning();
} }
@ -893,7 +868,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
if (payloads instanceof List) if (payloads instanceof List)
for (Object item : (List<?>) payloads) { for (Object item : (List<?>) payloads) {
if (Key.KEY_CREATED.equals(item)) { 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), getContentWarningDescription(context, status),
(TextUtils.isEmpty(status.getSpoilerText()) || !actionable.getSensitive() || status.isExpanded() ? status.getContent() : ""), (TextUtils.isEmpty(status.getSpoilerText()) || !actionable.getSensitive() || status.isExpanded() ? status.getContent() : ""),
getCreatedAtDescription(actionable.getCreatedAt(), statusDisplayOptions), getCreatedAtDescription(actionable.getCreatedAt(), statusDisplayOptions),
actionable.getEditedAt() != null ? context.getString(R.string.description_post_edited) : "",
getReblogDescription(context, status), getReblogDescription(context, status),
status.getUsername(), status.getUsername(),
actionable.getReblogged() ? context.getString(R.string.description_post_reblogged) : "", actionable.getReblogged() ? context.getString(R.string.description_post_reblogged) : "",

View File

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

View File

@ -53,6 +53,7 @@ import com.keylesspalace.tusky.EditProfileActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.StatusListActivity import com.keylesspalace.tusky.StatusListActivity
import com.keylesspalace.tusky.ViewMediaActivity 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.compose.ComposeActivity
import com.keylesspalace.tusky.components.report.ReportActivity import com.keylesspalace.tusky.components.report.ReportActivity
import com.keylesspalace.tusky.databinding.ActivityAccountBinding import com.keylesspalace.tusky.databinding.ActivityAccountBinding
@ -103,6 +104,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
private lateinit var accountFieldAdapter: AccountFieldAdapter private lateinit var accountFieldAdapter: AccountFieldAdapter
private val preferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
private var followState: FollowState = FollowState.NOT_FOLLOWING private var followState: FollowState = FollowState.NOT_FOLLOWING
private var blocking: Boolean = false private var blocking: Boolean = false
private var muting: Boolean = false private var muting: Boolean = false
@ -247,6 +250,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
val pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin) val pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin)
binding.accountFragmentViewPager.setPageTransformer(MarginPageTransformer(pageMargin)) 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 { binding.accountTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabReselected(tab: TabLayout.Tab?) { override fun onTabReselected(tab: TabLayout.Tab?) {
tab?.position?.let { position -> tab?.position?.let { position ->
@ -737,6 +743,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
menu.removeItem(R.id.action_report) 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) return super.onCreateOptionsMenu(menu)
} }
@ -849,6 +859,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
toggleMute() toggleMute()
return true return true
} }
R.id.action_add_or_remove_from_list -> {
ListsForAccountFragment.newInstance(viewModel.accountId).show(supportFragmentManager, null)
return true
}
R.id.action_mute_domain -> { R.id.action_mute_domain -> {
toggleBlockDomain(domain) toggleBlockDomain(domain)
return true 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 package com.keylesspalace.tusky.components.account.media
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.View import android.view.View
import androidx.core.app.ActivityOptionsCompat import androidx.core.app.ActivityOptionsCompat
import androidx.core.view.ViewCompat 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.openLink
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.AttachmentViewData
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -107,21 +105,30 @@ class AccountMediaFragment :
} }
adapter.addLoadStateListener { loadState -> 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) { if (adapter.itemCount == 0) {
binding.recyclerView.hide() when (loadState.refresh) {
binding.statusView.show() is LoadState.NotLoading -> {
val errorState = loadState.refresh as LoadState.Error if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) {
if (errorState.error is IOException) { binding.statusView.show()
binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network) { adapter.retry() } binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null)
} else { }
binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic) { adapter.retry() } }
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.PickMediaFiles
import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.afterTextChanged 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.getMediaSize
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.highlightSpans import com.keylesspalace.tusky.util.highlightSpans
import com.keylesspalace.tusky.util.loadAvatar import com.keylesspalace.tusky.util.loadAvatar
import com.keylesspalace.tusky.util.modernLanguageCode
import com.keylesspalace.tusky.util.onTextChanged import com.keylesspalace.tusky.util.onTextChanged
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
@ -269,7 +272,7 @@ class ComposeActivity :
binding.composeScheduleView.setDateTime(composeOptions?.scheduledAt) binding.composeScheduleView.setDateTime(composeOptions?.scheduledAt)
} }
setupLanguageSpinner(getInitialLanguage(composeOptions?.language)) setupLanguageSpinner(getInitialLanguage(composeOptions?.language, accountManager.activeAccount))
setupComposeField(preferences, viewModel.startingText) setupComposeField(preferences, viewModel.startingText)
setupDefaultTagViews(preferences) setupDefaultTagViews(preferences)
setupContentWarningField(composeOptions?.contentWarning) setupContentWarningField(composeOptions?.contentWarning)
@ -604,37 +607,19 @@ class ComposeActivity :
) )
} }
private fun setupLanguageSpinner(initialLanguage: String?) { 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
binding.composePostLanguageButton.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { binding.composePostLanguageButton.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) { 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<*>) { override fun onNothingSelected(parent: AdapterView<*>) {
parent.setSelection(locales.indexOfFirst { it.language == getInitialLanguage() }) parent.setSelection(0)
} }
} }
binding.composePostLanguageButton.apply { binding.composePostLanguageButton.apply {
adapter = LocaleAdapter(context, android.R.layout.simple_spinner_dropdown_item, locales) adapter = LocaleAdapter(context, android.R.layout.simple_spinner_dropdown_item, getLocaleList(initialLanguage))
setSelection(currentLocaleIndex) setSelection(0)
}
}
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
} }
} }
@ -874,7 +859,7 @@ class ComposeActivity :
// Wait until bottom sheet is not collapsed and show next screen after // Wait until bottom sheet is not collapsed and show next screen after
if (newState == BottomSheetBehavior.STATE_COLLAPSED) { if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
addMediaBehavior.removeBottomSheetCallback(this) 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( ActivityCompat.requestPermissions(
this@ComposeActivity, this@ComposeActivity,
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),

View File

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

View File

@ -68,11 +68,6 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
this.listener = listener; this.listener = listener;
} }
@Override
protected int getMediaPreviewHeight(Context context) {
return context.getResources().getDimensionPixelSize(R.dimen.status_media_preview_height);
}
void setupWithConversation( void setupWithConversation(
@NonNull ConversationViewData conversation, @NonNull ConversationViewData conversation,
@Nullable Object payloads @Nullable Object payloads
@ -88,7 +83,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
setDisplayName(account.getDisplayName(), account.getEmojis(), statusDisplayOptions); setDisplayName(account.getDisplayName(), account.getEmojis(), statusDisplayOptions);
setUsername(account.getUsername()); setUsername(account.getUsername());
setCreatedAt(status.getCreatedAt(), statusDisplayOptions); setCreatedAt(status.getCreatedAt(), status.getEditedAt(), statusDisplayOptions);
setIsReply(status.getInReplyToId() != null); setIsReply(status.getInReplyToId() != null);
setFavourited(status.getFavourited()); setFavourited(status.getFavourited());
setBookmarked(status.getBookmarked()); setBookmarked(status.getBookmarked());
@ -108,10 +103,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
} else { } else {
setMediaLabel(attachments, sensitive, listener, statusViewData.isShowingContent()); setMediaLabel(attachments, sensitive, listener, statusViewData.isShowingContent());
// Hide all unused views. // Hide all unused views.
mediaPreviews[0].setVisibility(View.GONE); mediaPreview.setVisibility(View.GONE);
mediaPreviews[1].setVisibility(View.GONE);
mediaPreviews[2].setVisibility(View.GONE);
mediaPreviews[3].setVisibility(View.GONE);
hideSensitiveMediaWarning(); hideSensitiveMediaWarning();
} }
@ -129,7 +121,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
if (payloads instanceof List) { if (payloads instanceof List) {
for (Object item : (List<?>) payloads) { for (Object item : (List<?>) payloads) {
if (Key.KEY_CREATED.equals(item)) { 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.PagingState
import androidx.paging.RemoteMediator import androidx.paging.RemoteMediator
import androidx.room.withTransaction import androidx.room.withTransaction
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.HttpHeaderLink import com.keylesspalace.tusky.util.HttpHeaderLink
@ -12,15 +13,17 @@ import retrofit2.HttpException
@OptIn(ExperimentalPagingApi::class) @OptIn(ExperimentalPagingApi::class)
class ConversationsRemoteMediator( class ConversationsRemoteMediator(
private val accountId: Long,
private val api: MastodonApi, private val api: MastodonApi,
private val db: AppDatabase private val db: AppDatabase,
accountManager: AccountManager,
) : RemoteMediator<Int, ConversationEntity>() { ) : RemoteMediator<Int, ConversationEntity>() {
private var nextKey: String? = null private var nextKey: String? = null
private var order: Int = 0 private var order: Int = 0
private val activeAccount = accountManager.activeAccount!!
override suspend fun load( override suspend fun load(
loadType: LoadType, loadType: LoadType,
state: PagingState<Int, ConversationEntity> state: PagingState<Int, ConversationEntity>
@ -46,7 +49,7 @@ class ConversationsRemoteMediator(
db.withTransaction { db.withTransaction {
if (loadType == LoadType.REFRESH) { if (loadType == LoadType.REFRESH) {
db.conversationDao().deleteForAccount(accountId) db.conversationDao().deleteForAccount(activeAccount.id)
} }
val linkHeader = conversationsResponse.headers()["Link"] val linkHeader = conversationsResponse.headers()["Link"]
@ -56,8 +59,19 @@ class ConversationsRemoteMediator(
db.conversationDao().insert( db.conversationDao().insert(
conversations conversations
.filterNot { it.lastStatus == null } .filterNot { it.lastStatus == null }
.map { .map { conversation ->
it.toEntity(accountId, order++)
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.db.AppDatabase
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.usecase.TimelineCases import com.keylesspalace.tusky.usecase.TimelineCases
import com.keylesspalace.tusky.util.EmptyPagingSource
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.rx3.await import kotlinx.coroutines.rx3.await
@ -42,8 +43,15 @@ class ConversationsViewModel @Inject constructor(
@OptIn(ExperimentalPagingApi::class) @OptIn(ExperimentalPagingApi::class)
val conversationFlow = Pager( val conversationFlow = Pager(
config = PagingConfig(pageSize = 30), config = PagingConfig(pageSize = 30),
remoteMediator = ConversationsRemoteMediator(accountManager.activeAccount!!.id, api, database), remoteMediator = ConversationsRemoteMediator(api, database, accountManager),
pagingSourceFactory = { database.conversationDao().conversationsForAccount(accountManager.activeAccount!!.id) } pagingSourceFactory = {
val activeAccount = accountManager.activeAccount
if (activeAccount == null) {
EmptyPagingSource()
} else {
database.conversationDao().conversationsForAccount(activeAccount.id)
}
}
) )
.flow .flow
.map { pagingData -> .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_SUBSCRIPTIONS = "CHANNEL_SUBSCRIPTIONS";
public static final String CHANNEL_SIGN_UP = "CHANNEL_SIGN_UP"; public static final String CHANNEL_SIGN_UP = "CHANNEL_SIGN_UP";
public static final String CHANNEL_UPDATES = "CHANNEL_UPDATES"; public static final String CHANNEL_UPDATES = "CHANNEL_UPDATES";
public static final String CHANNEL_REPORT = "CHANNEL_REPORT";
/** /**
* WorkManager Tag * WorkManager Tag
@ -401,6 +402,7 @@ public class NotificationHelper {
CHANNEL_SUBSCRIPTIONS + account.getIdentifier(), CHANNEL_SUBSCRIPTIONS + account.getIdentifier(),
CHANNEL_SIGN_UP + account.getIdentifier(), CHANNEL_SIGN_UP + account.getIdentifier(),
CHANNEL_UPDATES + account.getIdentifier(), CHANNEL_UPDATES + account.getIdentifier(),
CHANNEL_REPORT + account.getIdentifier(),
}; };
int[] channelNames = { int[] channelNames = {
R.string.notification_mention_name, R.string.notification_mention_name,
@ -412,6 +414,7 @@ public class NotificationHelper {
R.string.notification_subscription_name, R.string.notification_subscription_name,
R.string.notification_sign_up_name, R.string.notification_sign_up_name,
R.string.notification_update_name, R.string.notification_update_name,
R.string.notification_report_name,
}; };
int[] channelDescriptions = { int[] channelDescriptions = {
R.string.notification_mention_descriptions, R.string.notification_mention_descriptions,
@ -423,6 +426,7 @@ public class NotificationHelper {
R.string.notification_subscription_description, R.string.notification_subscription_description,
R.string.notification_sign_up_description, R.string.notification_sign_up_description,
R.string.notification_update_description, R.string.notification_update_description,
R.string.notification_report_description,
}; };
List<NotificationChannel> channels = new ArrayList<>(6); List<NotificationChannel> channels = new ArrayList<>(6);
@ -469,7 +473,7 @@ public class NotificationHelper {
if (notificationManager.areNotificationsEnabled()) { if (notificationManager.areNotificationsEnabled()) {
for (NotificationChannel channel : notificationManager.getNotificationChannels()) { for (NotificationChannel channel : notificationManager.getNotificationChannels()) {
if (channel.getImportance() > NotificationManager.IMPORTANCE_NONE) { if (channel != null && channel.getImportance() > NotificationManager.IMPORTANCE_NONE) {
Log.d(TAG, "NotificationsEnabled"); Log.d(TAG, "NotificationsEnabled");
return true; return true;
} }
@ -542,7 +546,7 @@ public class NotificationHelper {
return false; return false;
} }
NotificationChannel channel = notificationManager.getNotificationChannel(channelId); NotificationChannel channel = notificationManager.getNotificationChannel(channelId);
return channel.getImportance() > NotificationManager.IMPORTANCE_NONE; return channel != null && channel.getImportance() > NotificationManager.IMPORTANCE_NONE;
} }
switch (type) { switch (type) {
@ -564,6 +568,8 @@ public class NotificationHelper {
return account.getNotificationsSignUps(); return account.getNotificationsSignUps();
case UPDATE: case UPDATE:
return account.getNotificationsUpdates(); return account.getNotificationsUpdates();
case REPORT:
return account.getNotificationsReports();
default: default:
return false; return false;
} }
@ -593,6 +599,10 @@ public class NotificationHelper {
return CHANNEL_POLL + account.getIdentifier(); return CHANNEL_POLL + account.getIdentifier();
case SIGN_UP: case SIGN_UP:
return CHANNEL_SIGN_UP + account.getIdentifier(); return CHANNEL_SIGN_UP + account.getIdentifier();
case UPDATE:
return CHANNEL_UPDATES + account.getIdentifier();
case REPORT:
return CHANNEL_REPORT + account.getIdentifier();
default: default:
return null; return null;
} }
@ -678,6 +688,8 @@ public class NotificationHelper {
return String.format(context.getString(R.string.notification_sign_up_format), accountName); return String.format(context.getString(R.string.notification_sign_up_format), accountName);
case UPDATE: case UPDATE:
return String.format(context.getString(R.string.notification_update_format), accountName); 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; return null;
} }
@ -715,6 +727,12 @@ public class NotificationHelper {
} }
return builder.toString(); 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; return null;
} }

View File

@ -30,6 +30,7 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.TabPreferenceActivity import com.keylesspalace.tusky.TabPreferenceActivity
import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent 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.instancemute.InstanceListActivity
import com.keylesspalace.tusky.components.login.LoginActivity import com.keylesspalace.tusky.components.login.LoginActivity
import com.keylesspalace.tusky.components.notifications.currentAccountNeedsMigration 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.preferenceCategory
import com.keylesspalace.tusky.settings.switchPreference import com.keylesspalace.tusky.settings.switchPreference
import com.keylesspalace.tusky.util.ThemeUtils 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.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.colorInt
@ -66,6 +71,8 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
@Inject @Inject
lateinit var eventHub: EventHub lateinit var eventHub: EventHub
private val iconSize by lazy { resources.getDimensionPixelSize(R.dimen.preference_icon_size) }
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
val context = requireContext() val context = requireContext()
makePreferenceScreen { 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 { preference {
setTitle(R.string.action_view_mutes) setTitle(R.string.action_view_mutes)
setIcon(R.drawable.ic_mute_24dp) 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 { switchPreference {
setTitle(R.string.pref_default_media_sensitivity) setTitle(R.string.pref_default_media_sensitivity)
setIcon(R.drawable.ic_eye_24dp) setIcon(R.drawable.ic_eye_24dp)
@ -301,8 +345,8 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
} }
} }
private fun syncWithServer(visibility: String? = null, sensitive: Boolean? = null) { private fun syncWithServer(visibility: String? = null, sensitive: Boolean? = null, language: String? = null) {
mastodonApi.accountUpdateSource(visibility, sensitive) mastodonApi.accountUpdateSource(visibility, sensitive, language)
.enqueue(object : Callback<Account> { .enqueue(object : Callback<Account> {
override fun onResponse(call: Call<Account>, response: Response<Account>) { override fun onResponse(call: Call<Account>, response: Response<Account>) {
val account = response.body() val account = response.body()
@ -312,6 +356,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
it.defaultPostPrivacy = account.source?.privacy it.defaultPostPrivacy = account.source?.privacy
?: Status.Visibility.PUBLIC ?: Status.Visibility.PUBLIC
it.defaultMediaSensitivity = account.source?.sensitive ?: false it.defaultMediaSensitivity = account.source?.sensitive ?: false
it.defaultPostLanguage = language ?: ""
accountManager.saveAccount(it) accountManager.saveAccount(it)
} }
} else { } else {

View File

@ -144,6 +144,17 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Injectable {
true 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 -> preferenceCategory(R.string.pref_title_notification_alerts) { category ->

View File

@ -72,30 +72,17 @@ class PreferencesActivity :
setDisplayShowHomeEnabled(true) 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) val fragment: Fragment = supportFragmentManager.findFragmentByTag(fragmentTag)
?: when (intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)) { ?: when (preferenceType) {
GENERAL_PREFERENCES -> { GENERAL_PREFERENCES -> PreferencesFragment.newInstance()
setTitle(R.string.action_view_preferences) ACCOUNT_PREFERENCES -> AccountPreferencesFragment.newInstance()
PreferencesFragment.newInstance() NOTIFICATION_PREFERENCES -> NotificationPreferencesFragment.newInstance()
} TAB_FILTER_PREFERENCES -> TabFilterPreferencesFragment.newInstance()
ACCOUNT_PREFERENCES -> { PROXY_PREFERENCES -> ProxyPreferencesFragment.newInstance()
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()
}
else -> throw IllegalArgumentException("preferenceType not known") else -> throw IllegalArgumentException("preferenceType not known")
} }
@ -103,6 +90,14 @@ class PreferencesActivity :
replace(R.id.fragment_container, fragment, fragmentTag) 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) onBackPressedDispatcher.addCallback(this, restartActivitiesOnBackPressedCallback)
restartActivitiesOnBackPressedCallback.isEnabled = savedInstanceState?.getBoolean(EXTRA_RESTART_ON_BACK, false) ?: false restartActivitiesOnBackPressedCallback.isEnabled = savedInstanceState?.getBoolean(EXTRA_RESTART_ON_BACK, false) ?: false
} }
@ -141,10 +136,6 @@ class PreferencesActivity :
"enableSwipeForTabs", "mainNavPosition", PrefKeys.HIDE_TOP_TOOLBAR, "viewPagerOffScreenLimit" -> { "enableSwipeForTabs", "mainNavPosition", PrefKeys.HIDE_TOP_TOOLBAR, "viewPagerOffScreenLimit" -> {
restartActivitiesOnBackPressedCallback.isEnabled = true restartActivitiesOnBackPressedCallback.isEnabled = true
} }
"language" -> {
restartActivitiesOnBackPressedCallback.isEnabled = true
this.restartCurrentActivity()
}
} }
eventHub.dispatch(PreferenceChangedEvent(key)) 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.preference
import com.keylesspalace.tusky.settings.preferenceCategory import com.keylesspalace.tusky.settings.preferenceCategory
import com.keylesspalace.tusky.settings.switchPreference 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.deserialize
import com.keylesspalace.tusky.util.getNonNullString import com.keylesspalace.tusky.util.getNonNullString
import com.keylesspalace.tusky.util.makeIcon
import com.keylesspalace.tusky.util.serialize import com.keylesspalace.tusky.util.serialize
import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizePx
import de.c1710.filemojicompat_ui.views.picker.preference.EmojiPickerPreference import de.c1710.filemojicompat_ui.views.picker.preference.EmojiPickerPreference
import javax.inject.Inject import javax.inject.Inject
@ -49,6 +48,9 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
@Inject @Inject
lateinit var accountManager: AccountManager lateinit var accountManager: AccountManager
@Inject
lateinit var localeManager: LocaleManager
private val iconSize by lazy { resources.getDimensionPixelSize(R.dimen.preference_icon_size) } private val iconSize by lazy { resources.getDimensionPixelSize(R.dimen.preference_icon_size) }
private var httpProxyPref: Preference? = null private var httpProxyPref: Preference? = null
@ -77,10 +79,11 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
setDefaultValue("default") setDefaultValue("default")
setEntries(R.array.language_entries) setEntries(R.array.language_entries)
setEntryValues(R.array.language_values) setEntryValues(R.array.language_values)
key = PrefKeys.LANGUAGE key = PrefKeys.LANGUAGE + "_" // deliberately not the actual key, the real handling happens in LocaleManager
setSummaryProvider { entry } setSummaryProvider { entry }
setTitle(R.string.pref_title_language) setTitle(R.string.pref_title_language)
icon = makeIcon(GoogleMaterial.Icon.gmd_translate) icon = makeIcon(GoogleMaterial.Icon.gmd_translate)
preferenceDataStore = localeManager
} }
listPreference { listPreference {
@ -350,11 +353,7 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
} }
private fun makeIcon(icon: GoogleMaterial.Icon): IconicsDrawable { private fun makeIcon(icon: GoogleMaterial.Icon): IconicsDrawable {
val context = requireContext() return makeIcon(requireContext(), icon, iconSize)
return IconicsDrawable(context, icon).apply {
sizePx = iconSize
colorInt = ThemeUtils.getColor(context, R.attr.iconColor)
}
} }
override fun onResume() { override fun onResume() {

View File

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

View File

@ -23,6 +23,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Environment import android.os.Environment
import android.util.Log import android.util.Log
import android.view.View import android.view.View
@ -438,13 +439,21 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
} }
private fun requestDownloadAllMedia(status: Status) { private fun requestDownloadAllMedia(status: Status) {
val permissions = arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
(activity as BaseActivity).requestPermissions(permissions) { _, grantResults -> val permissions = arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { (activity as BaseActivity).requestPermissions(permissions) { _, grantResults ->
downloadAllMedia(status) if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
} else { downloadAllMedia(status)
Toast.makeText(context, R.string.error_media_download_permission, Toast.LENGTH_SHORT).show() } 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, inReplyToAccountId = null,
content = null, content = null,
createdAt = 0L, createdAt = 0L,
editedAt = 0L,
emojis = null, emojis = null,
reblogsCount = 0, reblogsCount = 0,
favouritesCount = 0, favouritesCount = 0,
@ -121,6 +122,7 @@ fun Status.toEntity(
inReplyToAccountId = actionableStatus.inReplyToAccountId, inReplyToAccountId = actionableStatus.inReplyToAccountId,
content = actionableStatus.content, content = actionableStatus.content,
createdAt = actionableStatus.createdAt.time, createdAt = actionableStatus.createdAt.time,
editedAt = actionableStatus.editedAt?.time,
emojis = actionableStatus.emojis.let(gson::toJson), emojis = actionableStatus.emojis.let(gson::toJson),
reblogsCount = actionableStatus.reblogsCount, reblogsCount = actionableStatus.reblogsCount,
favouritesCount = actionableStatus.favouritesCount, favouritesCount = actionableStatus.favouritesCount,
@ -173,6 +175,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
reblog = null, reblog = null,
content = status.content.orEmpty(), content = status.content.orEmpty(),
createdAt = Date(status.createdAt), createdAt = Date(status.createdAt),
editedAt = status.editedAt?.let { Date(it) },
emojis = emojis, emojis = emojis,
reblogsCount = status.reblogsCount, reblogsCount = status.reblogsCount,
favouritesCount = status.favouritesCount, favouritesCount = status.favouritesCount,
@ -205,6 +208,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
reblog = reblog, reblog = reblog,
content = "", content = "",
createdAt = Date(status.createdAt), // lie but whatever? createdAt = Date(status.createdAt), // lie but whatever?
editedAt = null,
emojis = listOf(), emojis = listOf(),
reblogsCount = 0, reblogsCount = 0,
favouritesCount = 0, favouritesCount = 0,
@ -236,6 +240,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
reblog = null, reblog = null,
content = status.content.orEmpty(), content = status.content.orEmpty(),
createdAt = Date(status.createdAt), createdAt = Date(status.createdAt),
editedAt = status.editedAt?.let { Date(it) },
emojis = emojis, emojis = emojis,
reblogsCount = status.reblogsCount, reblogsCount = status.reblogsCount,
favouritesCount = status.favouritesCount, 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.FilterModel
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.usecase.TimelineCases import com.keylesspalace.tusky.usecase.TimelineCases
import com.keylesspalace.tusky.util.EmptyPagingSource
import com.keylesspalace.tusky.viewdata.StatusViewData import com.keylesspalace.tusky.viewdata.StatusViewData
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor import kotlinx.coroutines.asExecutor
@ -89,7 +90,7 @@ class CachedTimelineViewModel @Inject constructor(
pagingSourceFactory = { pagingSourceFactory = {
val activeAccount = accountManager.activeAccount val activeAccount = accountManager.activeAccount
if (activeAccount == null) { if (activeAccount == null) {
EmptyTimelinePagingSource() EmptyPagingSource()
} else { } else {
db.timelineDao().getStatuses(activeAccount.id) db.timelineDao().getStatuses(activeAccount.id)
}.also { newPagingSource -> }.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 -> { 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) { adapter.submitList(uiState.statuses) {
if (viewModel.isInitialLoad) { if (viewModel.isInitialLoad) {
viewModel.isInitialLoad = false viewModel.isInitialLoad = false

View File

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

View File

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

View File

@ -154,6 +154,7 @@ class AccountManager @Inject constructor(db: AppDatabase) {
it.displayName = account.name it.displayName = account.name
it.profilePictureUrl = account.avatar it.profilePictureUrl = account.avatar
it.defaultPostPrivacy = account.source?.privacy ?: Status.Visibility.PUBLIC it.defaultPostPrivacy = account.source?.privacy ?: Status.Visibility.PUBLIC
it.defaultPostLanguage = account.source?.language ?: ""
it.defaultMediaSensitivity = account.source?.sensitive ?: false it.defaultMediaSensitivity = account.source?.sensitive ?: false
it.emojis = account.emojis ?: emptyList() 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, @Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
TimelineAccountEntity.class, ConversationEntity.class TimelineAccountEntity.class, ConversationEntity.class
}, version = 42) }, version = 45)
public abstract class AppDatabase extends RoomDatabase { public abstract class AppDatabase extends RoomDatabase {
public abstract AccountDao accountDao(); public abstract AccountDao accountDao();
@ -611,4 +611,26 @@ public abstract class AppDatabase extends RoomDatabase {
database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_language` TEXT"); 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( @Query(
""" """
SELECT s.serverId, s.url, s.timelineUserId, 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.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.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, 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 inReplyToAccountId: String?,
val content: String?, val content: String?,
val createdAt: Long, val createdAt: Long,
val editedAt: Long?,
val emojis: String?, val emojis: String?,
val reblogsCount: Int, val reblogsCount: Int,
val favouritesCount: 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.announcements.AnnouncementsActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.drafts.DraftsActivity import com.keylesspalace.tusky.components.drafts.DraftsActivity
import com.keylesspalace.tusky.components.followedtags.FollowedTagsActivity
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
import com.keylesspalace.tusky.components.login.LoginActivity import com.keylesspalace.tusky.components.login.LoginActivity
import com.keylesspalace.tusky.components.login.LoginWebViewActivity import com.keylesspalace.tusky.components.login.LoginWebViewActivity
@ -104,6 +105,9 @@ abstract class ActivitiesModule {
@ContributesAndroidInjector @ContributesAndroidInjector
abstract fun contributesFiltersActivity(): FiltersActivity abstract fun contributesFiltersActivity(): FiltersActivity
@ContributesAndroidInjector
abstract fun contributesFollowedTagsActivity(): FollowedTagsActivity
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class]) @ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
abstract fun contributesReportActivity(): ReportActivity 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_32_33, AppDatabase.MIGRATION_33_34, AppDatabase.MIGRATION_34_35,
AppDatabase.MIGRATION_35_36, AppDatabase.MIGRATION_36_37, AppDatabase.MIGRATION_37_38, 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_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() .build()
} }

View File

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

View File

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

View File

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

View File

@ -68,7 +68,9 @@ data class Attachment(
@Parcelize @Parcelize
data class MetaData( data class MetaData(
val focus: Focus?, val focus: Focus?,
val duration: Float? val duration: Float?,
val original: Size?,
val small: Size?,
) : Parcelable ) : Parcelable
/** /**
@ -82,4 +84,14 @@ data class Attachment(
val x: Float, val x: Float,
val y: Float val y: Float
) : Parcelable ) : 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 type: Type,
val id: String, val id: String,
val account: TimelineAccount, val account: TimelineAccount,
val status: Status? val status: Status?,
val report: Report?,
) { ) {
@JsonAdapter(NotificationTypeAdapter::class) @JsonAdapter(NotificationTypeAdapter::class)
@ -40,6 +41,7 @@ data class Notification(
STATUS("status"), STATUS("status"),
SIGN_UP("admin.sign_up"), SIGN_UP("admin.sign_up"),
UPDATE("update"), UPDATE("update"),
REPORT("admin.report"),
; ;
companion object { companion object {
@ -52,7 +54,7 @@ data class Notification(
} }
return UNKNOWN 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 { 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 reblog: Status?,
val content: String, val content: String,
@SerializedName("created_at", alternate = ["published"]) val createdAt: Date, @SerializedName("created_at", alternate = ["published"]) val createdAt: Date,
@SerializedName("edited_at") val editedAt: Date?,
val emojis: List<Emoji>, val emojis: List<Emoji>,
@SerializedName("reblogs_count") val reblogsCount: Int, @SerializedName("reblogs_count") val reblogsCount: Int,
@SerializedName("favourites_count") val favouritesCount: Int, @SerializedName("favourites_count") val favouritesCount: Int,

View File

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

View File

@ -25,6 +25,7 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Environment; import android.os.Environment;
import android.util.Log; import android.util.Log;
import android.view.Menu; import android.view.Menu;
@ -501,13 +502,17 @@ public abstract class SFragment extends Fragment implements Injectable {
} }
private void requestDownloadAllMedia(Status status) { private void requestDownloadAllMedia(Status status) {
String[] permissions = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}; if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
((BaseActivity) getActivity()).requestPermissions(permissions, (permissions1, grantResults) -> { String[] permissions = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE};
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { ((BaseActivity) getActivity()).requestPermissions(permissions, (permissions1, grantResults) -> {
downloadAllMedia(status); if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
} else { downloadAllMedia(status);
Toast.makeText(getContext(), R.string.error_media_download_permission, Toast.LENGTH_SHORT).show(); } 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.Animator
import android.animation.AnimatorListenerAdapter import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.text.method.ScrollingMovementMethod import android.text.method.ScrollingMovementMethod
import android.view.GestureDetector
import android.view.KeyEvent import android.view.KeyEvent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.MediaController import android.widget.MediaController
import androidx.core.view.GestureDetectorCompat
import com.keylesspalace.tusky.ViewMediaActivity import com.keylesspalace.tusky.ViewMediaActivity
import com.keylesspalace.tusky.databinding.FragmentViewVideoBinding import com.keylesspalace.tusky.databinding.FragmentViewVideoBinding
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.view.ExposedPlayPauseVideoView import com.keylesspalace.tusky.view.ExposedPlayPauseVideoView
import kotlin.math.abs
class ViewVideoFragment : ViewMediaFragment() { class ViewVideoFragment : ViewMediaFragment() {
interface VideoActionsListener {
fun onDismiss()
}
private var _binding: FragmentViewVideoBinding? = null private var _binding: FragmentViewVideoBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var videoActionsListener: VideoActionsListener
private lateinit var toolbar: View private lateinit var toolbar: View
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private val hideToolbar = Runnable { private val hideToolbar = Runnable {
@ -52,6 +61,11 @@ class ViewVideoFragment : ViewMediaFragment() {
private lateinit var mediaController: MediaController private lateinit var mediaController: MediaController
private var isAudio = false private var isAudio = false
override fun onAttach(context: Context) {
super.onAttach(context)
videoActionsListener = context as VideoActionsListener
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) { override fun setUserVisibleHint(isVisibleToUser: Boolean) {
// Start/pause/resume video playback as fragment is shown/hidden // Start/pause/resume video playback as fragment is shown/hidden
super.setUserVisibleHint(isVisibleToUser) super.setUserVisibleHint(isVisibleToUser)
@ -168,6 +182,7 @@ class ViewVideoFragment : ViewMediaFragment() {
return binding.root return binding.root
} }
@SuppressLint("ClickableViewAccessibility")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val attachment = arguments?.getParcelable<Attachment>(ARG_ATTACHMENT) val attachment = arguments?.getParcelable<Attachment>(ARG_ATTACHMENT)
@ -177,6 +192,54 @@ class ViewVideoFragment : ViewMediaFragment() {
} }
val url = attachment.url val url = attachment.url
isAudio = attachment.type == Attachment.Type.AUDIO 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) 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") @PATCH("api/v1/accounts/update_credentials")
fun accountUpdateSource( fun accountUpdateSource(
@Field("source[privacy]") privacy: String?, @Field("source[privacy]") privacy: String?,
@Field("source[sensitive]") sensitive: Boolean? @Field("source[sensitive]") sensitive: Boolean?,
@Field("source[language]") language: String?,
): Call<Account> ): Call<Account>
@Multipart @Multipart
@ -481,6 +482,11 @@ interface MastodonApi {
@GET("/api/v1/lists") @GET("/api/v1/lists")
suspend fun getLists(): NetworkResult<List<MastoList>> suspend fun getLists(): NetworkResult<List<MastoList>>
@GET("/api/v1/accounts/{id}/lists")
suspend fun getListsIncludesAccount(
@Path("id") accountId: String
): NetworkResult<List<MastoList>>
@FormUrlEncoded @FormUrlEncoded
@POST("api/v1/lists") @POST("api/v1/lists")
suspend fun createList( suspend fun createList(
@ -668,6 +674,14 @@ interface MastodonApi {
@GET("api/v1/tags/{name}") @GET("api/v1/tags/{name}")
suspend fun tag(@Path("name") name: String): NetworkResult<HashTag> 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") @POST("api/v1/tags/{name}/follow")
suspend fun followTag(@Path("name") name: String): NetworkResult<HashTag> suspend fun followTag(@Path("name") name: String): NetworkResult<HashTag>

View File

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

View File

@ -1,4 +1,5 @@
@file:JvmName("AttachmentHelper") @file:JvmName("AttachmentHelper")
package com.keylesspalace.tusky.util package com.keylesspalace.tusky.util
import android.content.Context import android.content.Context
@ -24,3 +25,12 @@ private fun formatDuration(durationInSeconds: Double): String {
val hours = durationInSeconds.toInt() / 3600 val hours = durationInSeconds.toInt() / 3600
return "%d:%02d:%02d".format(hours, minutes, seconds) 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.HashTag
import com.keylesspalace.tusky.entity.Status.Mention import com.keylesspalace.tusky.entity.Status.Mention
import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.interfaces.LinkListener
import java.net.URI
import java.net.URISyntaxException
fun getDomain(urlString: String?): String { fun getDomain(urlString: String?): String {
val host = urlString?.toUri()?.host val host = urlString?.toUri()?.host
@ -72,22 +74,16 @@ fun markupHiddenUrls(context: Context, content: CharSequence): SpannableStringBu
val spannableContent = SpannableStringBuilder(content) val spannableContent = SpannableStringBuilder(content)
val originalSpans = spannableContent.getSpans(0, content.length, URLSpan::class.java) val originalSpans = spannableContent.getSpans(0, content.length, URLSpan::class.java)
val obscuredLinkSpans = originalSpans.filter { val obscuredLinkSpans = originalSpans.filter {
val text = spannableContent.subSequence(spannableContent.getSpanStart(it), spannableContent.getSpanEnd(it)) val start = spannableContent.getSpanStart(it)
val firstCharacter = text[0] val firstCharacter = content[start]
return@filter if (firstCharacter == '#' || firstCharacter == '@') { return@filter if (firstCharacter == '#' || firstCharacter == '@') {
false false
} else { } else {
val lastPart = text.toString().split(' ').lastOrNull() ?: "" val text = spannableContent.subSequence(start, spannableContent.getSpanEnd(it)).toString()
var textDomain = getDomain(lastPart) .split(' ').lastOrNull() ?: ""
var textDomain = getDomain(text)
if (textDomain.isBlank()) { if (textDomain.isBlank()) {
// Allow "some.domain" or "www.some.domain" without a domain notifier textDomain = getDomain("https://$text")
textDomain = lastPart
if (textDomain.startsWith("www.")) {
textDomain = textDomain.substring(4)
}
if (textDomain.endsWith("/")) {
textDomain = textDomain.substring(0, textDomain.length - 1)
}
} }
getDomain(it.url) != textDomain 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" 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.Context
import android.content.SharedPreferences 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 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) private var prefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
fun setLocale(context: Context): Context { fun setLocale() {
val language = prefs.getNonNullString("language", "default") val language = prefs.getNonNullString(PrefKeys.LANGUAGE, DEFAULT)
if (language == "default") {
return context
}
val locale = Locale.forLanguageTag(language)
Locale.setDefault(locale)
val res = context.resources if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val config = Configuration(res.configuration) if (language != HANDLED_BY_SYSTEM) {
config.setLocale(locale) // app is being opened on Android 13+ for the first time
return context.createConfigurationContext(config) // 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) { public static String getRelativeTimeSpanString(Context context, long then, long now) {
long span = now - then; long span = now - then;
boolean future = false; 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; future = true;
span = -span; span = -span;
} }

View File

@ -47,6 +47,7 @@ fun Notification.toViewData(
this.type, this.type,
this.id, this.id,
this.account, 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 androidx.annotation.Nullable;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Notification; import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.entity.Report;
import com.keylesspalace.tusky.entity.TimelineAccount; import com.keylesspalace.tusky.entity.TimelineAccount;
import java.util.Objects; import java.util.Objects;
@ -48,13 +48,16 @@ public abstract class NotificationViewData {
private final TimelineAccount account; private final TimelineAccount account;
@Nullable @Nullable
private final StatusViewData.Concrete statusViewData; private final StatusViewData.Concrete statusViewData;
@Nullable
private final Report report;
public Concrete(Notification.Type type, String id, TimelineAccount account, public Concrete(Notification.Type type, String id, TimelineAccount account,
@Nullable StatusViewData.Concrete statusViewData) { @Nullable StatusViewData.Concrete statusViewData, @Nullable Report report) {
this.type = type; this.type = type;
this.id = id; this.id = id;
this.account = account; this.account = account;
this.statusViewData = statusViewData; this.statusViewData = statusViewData;
this.report = report;
} }
public Notification.Type getType() { public Notification.Type getType() {
@ -74,6 +77,11 @@ public abstract class NotificationViewData {
return statusViewData; return statusViewData;
} }
@Nullable
public Report getReport() {
return report;
}
@Override @Override
public long getViewDataId() { public long getViewDataId() {
return id.hashCode(); return id.hashCode();
@ -87,7 +95,8 @@ public abstract class NotificationViewData {
return type == concrete.type && return type == concrete.type &&
Objects.equals(id, concrete.id) && Objects.equals(id, concrete.id) &&
account.getId().equals(concrete.account.getId()) && account.getId().equals(concrete.account.getId()) &&
(Objects.equals(statusViewData, concrete.statusViewData)); (Objects.equals(statusViewData, concrete.statusViewData)) &&
(Objects.equals(report, concrete.report));
} }
@Override @Override
@ -97,7 +106,7 @@ public abstract class NotificationViewData {
} }
public Concrete copyWithStatus(@Nullable StatusViewData.Concrete statusViewData) { 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:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
app:title="@string/title_view_thread" app:menu="@menu/view_thread_toolbar"
app:navigationIcon="?attr/homeAsUpIndicator"
app:navigationContentDescription="@string/abc_action_bar_up_description" 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> </com.google.android.material.appbar.AppBarLayout>
@ -25,8 +25,8 @@
android:id="@+id/swipeRefreshLayout" android:id="@+id/swipeRefreshLayout"
android:layout_width="640dp" android:layout_width="640dp"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" android:layout_gravity="center_horizontal|top"
android:layout_gravity="center_horizontal"> app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView" android:id="@+id/recyclerView"
@ -47,7 +47,7 @@
android:id="@+id/statusView" android:id="@+id/statusView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="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> </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 <androidx.appcompat.widget.Toolbar
android:id="@+id/mainToolbar" android:id="@+id/mainToolbar"
style="@style/Widget.AppCompat.Toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:contentInsetStartWithNavigation="0dp" app:contentInsetStartWithNavigation="0dp"
app:layout_scrollFlags="scroll|enterAlways" app:layout_scrollFlags="scroll|enterAlways"
app:navigationContentDescription="@string/action_open_drawer" /> app:navigationContentDescription="@string/action_open_drawer" />
<com.google.android.material.tabs.TabLayout <LinearLayout
android:id="@+id/tabLayout" android:id="@+id/topNav"
style="@style/TuskyTabAppearance"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:tabGravity="fill" android:orientation="horizontal">
app:tabMaxWidth="0dp"
app:tabMode="fixed" /> <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> </com.google.android.material.appbar.AppBarLayout>
@ -61,13 +79,30 @@
app:contentInsetStart="0dp" app:contentInsetStart="0dp"
app:fabAlignmentMode="end"> app:fabAlignmentMode="end">
<com.google.android.material.tabs.TabLayout <LinearLayout
android:id="@+id/bottomTabLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="wrap_content"
app:tabIndicator="@null" android:orientation="horizontal">
app:tabGravity="fill"
app:tabMode="fixed" /> <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> </com.google.android.material.bottomappbar.BottomAppBar>
@ -87,8 +122,8 @@
android:id="@+id/progressBar" android:id="@+id/progressBar"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="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> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -29,6 +29,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:layout_margin="16dp" android:layout_margin="16dp"
android:contentDescription="@string/action_add_tab"
android:src="@drawable/ic_plus_24dp" /> android:src="@drawable/ic_plus_24dp" />
<com.google.android.material.card.MaterialCardView <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:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
app:title="@string/title_view_thread" app:menu="@menu/view_thread_toolbar"
app:navigationIcon="?attr/homeAsUpIndicator"
app:navigationContentDescription="@string/abc_action_bar_up_description" 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> </com.google.android.material.appbar.AppBarLayout>
@ -25,8 +25,8 @@
android:id="@+id/swipeRefreshLayout" android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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 <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView" android:id="@+id/recyclerView"
@ -47,7 +47,7 @@
android:id="@+id/statusView" android:id="@+id/statusView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="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> </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_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="8dp" android:padding="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text"> app:layout_constraintTop_toBottomOf="@id/text">
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
@ -30,6 +30,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:checkable="false" android:checkable="false"
android:contentDescription="@string/action_add_reaction"
app:chipEndPadding="4dp" app:chipEndPadding="4dp"
app:chipIcon="@drawable/ic_plus_24dp" app:chipIcon="@drawable/ic_plus_24dp"
app:chipSurfaceColor="@color/tusky_blue" app:chipSurfaceColor="@color/tusky_blue"

View File

@ -4,8 +4,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="16dp" android:paddingStart="16dp"
android:paddingRight="16dp"
android:paddingBottom="10dp"> android:paddingBottom="10dp">
<TextView <TextView
@ -13,7 +12,6 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:drawableStart="@drawable/ic_person_add_24dp"
android:drawablePadding="10dp" android:drawablePadding="10dp"
android:ellipsize="end" android:ellipsize="end"
android:gravity="center_vertical" android:gravity="center_vertical"
@ -21,6 +19,7 @@
android:paddingStart="28dp" android:paddingStart="28dp"
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium" android:textSize="?attr/status_text_medium"
app:drawableStartCompat="@drawable/ic_person_add_24dp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:text="Someone requested to follow you" /> tools:text="Someone requested to follow you" />
@ -32,13 +31,13 @@
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:contentDescription="@string/action_view_profile" android:contentDescription="@string/action_view_profile"
tools:src="@drawable/avatar_default"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/notificationTextView" /> app:layout_constraintTop_toBottomOf="@id/notificationTextView"
tools:src="@drawable/avatar_default" />
<TextView <TextView
android:id="@+id/displayNameTextView" android:id="@+id/displayNameTextView"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="14dp" android:layout_marginStart="14dp"
android:ellipsize="end" android:ellipsize="end"
@ -46,54 +45,56 @@
android:textColor="?android:textColorPrimary" android:textColor="?android:textColorPrimary"
android:textSize="?attr/status_text_large" android:textSize="?attr/status_text_large"
android:textStyle="normal|bold" android:textStyle="normal|bold"
app:layout_constraintBottom_toTopOf="@id/usernameTextView"
app:layout_constraintEnd_toStartOf="@id/rejectButton"
app:layout_constraintStart_toEndOf="@id/avatar" app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toTopOf="@id/avatar" app:layout_constraintTop_toTopOf="@id/avatar"
app:layout_constraintBottom_toTopOf="@id/usernameTextView"
tools:text="Display name" /> tools:text="Display name" />
<TextView <TextView
android:id="@+id/usernameTextView" android:id="@+id/usernameTextView"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="14dp" android:layout_marginStart="14dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium" 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_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toBottomOf="@id/displayNameTextView" app:layout_constraintTop_toBottomOf="@id/displayNameTextView"
app:layout_constraintBottom_toBottomOf="@id/avatar"
tools:text="\@username" /> 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 <ImageButton
android:id="@+id/acceptButton" android:id="@+id/acceptButton"
style="@style/TuskyImageButton" style="@style/TuskyImageButton"
android:layout_width="32dp" android:layout_width="52dp"
android:layout_height="32dp" android:layout_height="48dp"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_marginStart="12dp" android:layout_marginStart="12dp"
android:layout_marginEnd="4dp" android:layout_marginEnd="4dp"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_accept" android:contentDescription="@string/action_accept"
android:padding="4dp" android:padding="4dp"
app:layout_constraintEnd_toStartOf="@id/rejectButton"
app:layout_constraintTop_toTopOf="@id/avatar"
app:layout_constraintBottom_toBottomOf="@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_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/avatar" app:layout_constraintTop_toTopOf="@id/avatar"
app:layout_constraintBottom_toBottomOf="@id/avatar" app:srcCompat="@drawable/ic_check_24dp" />
app:srcCompat="@drawable/ic_reject_24dp" />
</androidx.constraintlayout.widget.ConstraintLayout> </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"?> <?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" <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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout">
<com.keylesspalace.tusky.view.MediaPreviewImageView <com.keylesspalace.tusky.view.MediaPreviewLayout
android:id="@+id/status_media_preview_0" android:id="@+id/status_media_preview"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="@dimen/status_media_preview_height" android:layout_height="wrap_content"
android:scaleType="centerCrop" app:layout_constraintEnd_toStartOf="@+id/status_media_preview_1"
app:layout_constraintEnd_toStartOf="@+id/status_media_preview_1" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<com.keylesspalace.tusky.view.MediaPreviewImageView <ImageView
android:id="@+id/status_media_preview_1" android:id="@+id/status_sensitive_media_button"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="@dimen/status_media_preview_height" android:layout_height="wrap_content"
android:layout_marginStart="4dp" android:alpha="0.7"
android:scaleType="centerCrop" android:contentDescription="@null"
app:layout_constraintEnd_toEndOf="parent" android:padding="@dimen/status_sensitive_media_button_padding"
app:layout_constraintStart_toEndOf="@+id/status_media_preview_0" android:visibility="gone"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintLeft_toLeftOf="@+id/status_media_preview_container"
tools:ignore="ContentDescription" /> app:layout_constraintTop_toTopOf="@+id/status_media_preview_container"
app:srcCompat="@drawable/ic_eye_24dp"
app:tint="@color/white" />
<com.keylesspalace.tusky.view.MediaPreviewImageView <TextView
android:id="@+id/status_media_preview_2" android:id="@+id/status_sensitive_media_warning"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="@dimen/status_media_preview_height" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:background="@drawable/media_warning_bg"
android:scaleType="centerCrop" android:gravity="center"
app:layout_constraintEnd_toStartOf="@+id/status_media_preview_3" android:lineSpacingMultiplier="1.2"
app:layout_constraintStart_toStartOf="parent" android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@+id/status_media_preview_0" android:paddingLeft="12dp"
tools:ignore="ContentDescription" /> 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 <TextView
android:id="@+id/status_media_preview_3" android:id="@+id/status_media_label_0"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="@dimen/status_media_preview_height" android:layout_height="wrap_content"
android:layout_marginStart="4dp" android:background="?attr/selectableItemBackground"
android:layout_marginTop="4dp" android:drawablePadding="4dp"
android:background="@drawable/media_preview_outline" android:ellipsize="end"
android:scaleType="centerCrop" android:gravity="center_vertical"
app:layout_constraintEnd_toEndOf="parent" android:importantForAccessibility="no"
app:layout_constraintStart_toEndOf="@+id/status_media_preview_2" android:maxLines="10"
app:layout_constraintTop_toBottomOf="@+id/status_media_preview_1" android:textSize="?attr/status_text_medium"
tools:ignore="ContentDescription" /> android:visibility="gone"
app:drawableTint="?android:attr/textColorTertiary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView <TextView
android:id="@+id/status_media_overlay_0" android:id="@+id/status_media_label_1"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="0dp" android:layout_height="wrap_content"
android:scaleType="center" android:background="?attr/selectableItemBackground"
app:layout_constraintBottom_toBottomOf="@+id/status_media_preview_0" android:drawablePadding="4dp"
app:layout_constraintEnd_toEndOf="@+id/status_media_preview_0" android:ellipsize="end"
app:layout_constraintStart_toStartOf="@+id/status_media_preview_0" android:gravity="center_vertical"
app:layout_constraintTop_toTopOf="@+id/status_media_preview_0" android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_play_indicator" android:maxLines="10"
tools:ignore="ContentDescription" /> 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 <TextView
android:id="@+id/status_media_overlay_1" android:id="@+id/status_media_label_2"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="0dp" android:layout_height="wrap_content"
android:scaleType="center" android:background="?attr/selectableItemBackground"
app:layout_constraintBottom_toBottomOf="@+id/status_media_preview_1" android:drawablePadding="4dp"
app:layout_constraintEnd_toEndOf="@+id/status_media_preview_1" android:ellipsize="end"
app:layout_constraintStart_toStartOf="@+id/status_media_preview_1" android:gravity="center_vertical"
app:layout_constraintTop_toTopOf="@+id/status_media_preview_1" android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_play_indicator" android:maxLines="10"
tools:ignore="ContentDescription" /> 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 <TextView
android:id="@+id/status_media_overlay_2" android:id="@+id/status_media_label_3"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="0dp" android:layout_height="wrap_content"
android:scaleType="center" android:background="?attr/selectableItemBackground"
app:layout_constraintBottom_toBottomOf="@+id/status_media_preview_2" android:drawablePadding="4dp"
app:layout_constraintEnd_toEndOf="@+id/status_media_preview_2" android:ellipsize="end"
app:layout_constraintStart_toStartOf="@+id/status_media_preview_2" android:gravity="center_vertical"
app:layout_constraintTop_toTopOf="@+id/status_media_preview_2" android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_play_indicator" android:maxLines="10"
tools:ignore="ContentDescription" /> android:textSize="?attr/status_text_medium"
android:visibility="gone"
<ImageView app:drawableTint="?android:attr/textColorTertiary"
android:id="@+id/status_media_overlay_3" app:layout_constraintStart_toStartOf="parent"
android:layout_width="0dp" app:layout_constraintTop_toBottomOf="@id/status_media_label_2" />
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" />
</merge> </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" android:title="@string/action_block"
app:showAsAction="never" /> 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" <item android:id="@+id/action_mute_domain"
android:title="@string/action_mute_domain" android:title="@string/action_mute_domain"
app:showAsAction="never" /> app:showAsAction="never" />
@ -29,4 +33,4 @@
<item <item
android:id="@+id/action_report" android:id="@+id/action_report"
android:title="@string/action_report" /> android:title="@string/action_report" />
</menu> </menu>

View File

@ -17,5 +17,18 @@
app:iconTint="?attr/colorOnSurface" app:iconTint="?attr/colorOnSurface"
android:icon="@drawable/ic_person_remove_24dp" /> 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> </menu>

View File

@ -2,29 +2,29 @@
<resources> <resources>
<string name="error_generic">Vyskytla se chyba.</string> <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_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_empty">Toto nemůže být prázdné.</string>
<string name="error_invalid_domain">Neplatná doména zadána</string> <string name="error_invalid_domain">Byla zadána neplatná doména</string>
<string name="error_failed_app_registration">Autentizace s tímto serverem neuspěla.</string> <string name="error_failed_app_registration">Autentizace s tímto serverem nebyla úspěšná.</string>
<string name="error_no_web_browser_found">Nelze najít webový prohlížeč k použití.</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_unknown">Vyskytla se neidentifikovaná chyba autorizace.</string>
<string name="error_authorization_denied">Autorizace byla zamítnuta.</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_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_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_opening">Tento soubor se nepodařilo otevřít.</string>
<string name="error_media_upload_permission">Je vyžadováno povolení číst média.</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 povolení ukládat média.</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 tootu nemohou být přiloženy obrázky i videa.</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í selhalo.</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í tootu.</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_home">Domů</string>
<string name="title_notifications">Oznámení</string> <string name="title_notifications">Oznámení</string>
<string name="title_public_local">Místní</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_direct_messages">Přímé zprávy</string>
<string name="title_tab_preferences">Panely</string> <string name="title_tab_preferences">Panely</string>
<string name="title_view_thread">Toot</string> <string name="title_view_thread">Vlákno</string>
<string name="title_posts">Tooty</string> <string name="title_posts">Příspěvky</string>
<string name="title_posts_with_replies">S odpověďmi</string> <string name="title_posts_with_replies">S odpověďmi</string>
<string name="title_posts_pinned">Připnuté</string> <string name="title_posts_pinned">Připnuté</string>
<string name="title_follows">Sledovaní</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_more">Zobrazit více</string>
<string name="post_content_warning_show_less">Zobrazit méně</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_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="message_empty">Tady nic není.</string>
<string name="footer_empty">Tady nic není. Obnovte přetažnením dolů!</string> <string name="footer_empty">Tady nic není. Obnovte přetažením dolů!</string>
<string name="notification_reblog_format">%s boostnul/a váš toot</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áš toot</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="notification_follow_format">%s vás nyní sleduje</string>
<string name="report_username_format">Nahlásit uživatele @%s</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_quick_reply">Rychlá odpověď</string>
<string name="action_reply">Odpovědět</string> <string name="action_reply">Odpovědět</string>
<string name="action_reblog">Boostnout</string> <string name="action_reblog">Boostnout</string>
<string name="action_unreblog">Odstranit boost</string> <string name="action_unreblog">Odstranit boost</string>
<string name="action_favourite">Oblíbit</string> <string name="action_favourite">Oblíbit</string>
<string name="action_unfavourite">Odstranit oblíbení</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_compose">Napsat</string>
<string name="action_login">Přihlásit účtem Mastodon</string> <string name="action_login">Přihlásit se účtem Mastodon</string>
<string name="action_logout">Odhlásit</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_logout_confirm">Jste si jistý/á, že se chcete odhlásit z účtu %1$s?</string>
<string name="action_follow">Sledovat</string> <string name="action_follow">Sledovat</string>
<string name="action_unfollow">Přestat sledovat</string> <string name="action_unfollow">Přestat sledovat</string>
<string name="action_block">Blokovat</string> <string name="action_block">Blokovat</string>
<string name="action_unblock">Odblokovat</string> <string name="action_unblock">Odblokovat</string>
<string name="action_hide_reblogs">Skrýt boosty</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_report">Nahlásit</string>
<string name="action_delete">Smazat</string> <string name="action_delete">Smazat</string>
<string name="action_send">TOOTNOUT</string> <string name="action_send">TOOTNOUT</string>
@ -85,10 +85,10 @@
<string name="action_view_media">Média</string> <string name="action_view_media">Média</string>
<string name="action_open_in_web">Otevřít v prohlížeči</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_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_share">Sdílet</string>
<string name="action_mute">Skrýt</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_mention">Zmínit</string>
<string name="action_hide_media">Skrýt média</string> <string name="action_hide_media">Skrýt média</string>
<string name="action_open_drawer">Otevřít menu</string> <string name="action_open_drawer">Otevřít menu</string>
@ -100,7 +100,7 @@
<string name="action_reject">Zamítnout</string> <string name="action_reject">Zamítnout</string>
<string name="action_search">Hledat</string> <string name="action_search">Hledat</string>
<string name="action_access_drafts">Koncepty</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_content_warning">Varování o obsahu</string>
<string name="action_emoji_keyboard">Klávesnice s emoji</string> <string name="action_emoji_keyboard">Klávesnice s emoji</string>
<string name="action_add_tab">Přidat panel</string> <string name="action_add_tab">Přidat panel</string>
@ -109,7 +109,7 @@
<string name="action_hashtags">Hashtagy</string> <string name="action_hashtags">Hashtagy</string>
<string name="action_open_reblogger">Otevřít autora boostu</string> <string name="action_open_reblogger">Otevřít autora boostu</string>
<string name="action_open_reblogged_by">Zobrazit boosty</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_hashtags_dialog">Hashtagy</string>
<string name="title_mentions_dialog">Zmínky</string> <string name="title_mentions_dialog">Zmínky</string>
<string name="title_links_dialog">Odkazy</string> <string name="title_links_dialog">Odkazy</string>
@ -120,12 +120,12 @@
<string name="action_share_as">Sdílet jako…</string> <string name="action_share_as">Sdílet jako…</string>
<string name="download_media">Stáhnout média</string> <string name="download_media">Stáhnout média</string>
<string name="downloading_media">Stahuji 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_link_to">Sdílet URL příspěvku na…</string>
<string name="send_post_content_to">Sdílet toot 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="send_media_to">Sdílet média na…</string>
<string name="confirmation_reported">Odesláno!</string> <string name="confirmation_reported">Odesláno!</string>
<string name="confirmation_unblocked">Uživatel odblokován</string> <string name="confirmation_unblocked">Uživatel byl odblokován</string>
<string name="confirmation_unmuted">Uživatel odkryt</string> <string name="confirmation_unmuted">Skrytí uživatele bylo zrušeno</string>
<string name="post_sent">Odesláno!</string> <string name="post_sent">Odesláno!</string>
<string name="post_sent_long">Odpověď byla úspěšně odeslána.</string> <string name="post_sent_long">Odpověď byla úspěšně odeslána.</string>
<string name="hint_domain">Který server?</string> <string name="hint_domain">Který server?</string>
@ -138,7 +138,7 @@
<string name="label_quick_reply">Odpovědět…</string> <string name="label_quick_reply">Odpovědět…</string>
<string name="label_avatar">Avatar</string> <string name="label_avatar">Avatar</string>
<string name="label_header">Záhlaví</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="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 <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 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_download_image">Stáhnout</string>
<string name="dialog_message_cancel_follow_request">Zrušit požadavek o sledování?</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_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_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_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_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_edit_notification_settings">Oznámení</string>
<string name="pref_title_notifications_enabled">Oznámení</string> <string name="pref_title_notifications_enabled">Oznámení</string>
<string name="pref_title_notification_alerts">Upozornění</string> <string name="pref_title_notification_alerts">Upozornění</string>
@ -177,7 +177,7 @@
<string name="app_them_dark">Tmavý</string> <string name="app_them_dark">Tmavý</string>
<string name="app_theme_light">Světlý</string> <string name="app_theme_light">Světlý</string>
<string name="app_theme_black">Černý</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="app_theme_system">Použít systémový design</string>
<string name="pref_title_browser_settings">Prohlížeč</string> <string name="pref_title_browser_settings">Prohlížeč</string>
<string name="pref_title_custom_tabs">Používat Vlastní karty Chrome</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_language">Jazyk</string>
<string name="pref_title_post_filter">Filtrování časových os</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_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_replies">Zobrazit odpovědi</string>
<string name="pref_title_show_media_preview">Stahovat náhledy médií</string> <string name="pref_title_show_media_preview">Stahovat náhledy médií</string>
<string name="pref_title_proxy_settings">Proxy</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_name">Noví sledující</string>
<string name="notification_follow_description">Oznámení o nových sledujících</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_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_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_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_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_medium">%1$s, %2$s a %3$s</string>
<string name="notification_summary_small">%1$s a %2$s</string> <string name="notification_summary_small">%1$s a %2$s</string>
<plurals name="notification_title_summary"> <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> <item quantity="other">%d nových interakcí</item>
</plurals> </plurals>
<string name="description_account_locked">Uzamčený účet</string> <string name="description_account_locked">Uzamčený účet</string>
@ -240,8 +242,8 @@
https://github.com/accelforce/Yuito/issues https://github.com/accelforce/Yuito/issues
</string> </string>
<string name="about_tusky_account">Profil aplikace Yuito</string> <string name="about_tusky_account">Profil aplikace Yuito</string>
<string name="post_share_content">Sdílet obsah tootu</string> <string name="post_share_content">Sdílet obsah příspěvku</string>
<string name="post_share_link">Sdílet odkaz k tootu</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_images">Obrázky</string>
<string name="post_media_video">Video</string> <string name="post_media_video">Video</string>
<string name="state_follow_requested">Vyžádáno sledování</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_years">za %d let</string>
<string name="abbreviated_in_days">za %d d</string> <string name="abbreviated_in_days">za %d d</string>
<string name="abbreviated_in_hours">za %d h</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_in_seconds">za %d s</string>
<string name="abbreviated_years_ago">%d let</string> <string name="abbreviated_years_ago">%d let</string>
<string name="abbreviated_days_ago">%d d</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="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_add_to_list">Přidat účet na seznam</string>
<string name="action_remove_from_list">Odstranit účet ze seznamu</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> <string name="error_failed_set_caption">Nastavení popisku selhalo</string>
<plurals name="hint_describe_for_visually_impaired"> <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> </plurals>
<string name="action_set_caption">Nastavit popisek</string> <string name="action_set_caption">Nastavit popisek</string>
<string name="action_remove">Odstranit</string> <string name="action_remove">Odstranit</string>
<string name="lock_account_label">Uzamknout účet</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="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="compose_save_draft">Uložit koncept?</string>
<string name="send_post_notification_title">Odesílám toot</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í tootu</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 tooty</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_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="action_compose_shortcut">Napsat</string>
<string name="error_no_custom_emojis">Vaše instance %s nemá žádná vlastní emoji</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="emoji_style">Styl emoji</string>
<string name="system_default">Výchozí nastavení systému</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="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="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="expand_collapse_all_posts">Rozbalit/Sbalit všechny příspěvky</string>
<string name="action_open_post">Otevřít toot</string> <string name="action_open_post">Otevřít příspěvek</string>
<string name="restart_required">Je vyžadován restart aplikace</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="restart_emoji">Pro použití těchto změn musíte restartovat aplikaci Yuito</string>
<string name="later">Později</string> <string name="later">Později</string>
<string name="restart">Restartovat</string> <string name="restart">Restartovat</string>
@ -326,7 +333,7 @@
<string name="profile_metadata_label_label">Označení</string> <string name="profile_metadata_label_label">Označení</string>
<string name="profile_metadata_content_label">Obsah</string> <string name="profile_metadata_content_label">Obsah</string>
<string name="pref_title_absolute_time">Používat absolutní čas</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="unpin_action">Odepnout</string>
<string name="pin_action">Připnout</string> <string name="pin_action">Připnout</string>
<plurals name="favs"> <plurals name="favs">
@ -336,7 +343,7 @@
</plurals> </plurals>
<plurals name="reblogs"> <plurals name="reblogs">
<item quantity="one"><b>%s</b> boost</item> <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> <item quantity="other"><b>%s</b> boostů</item>
</plurals> </plurals>
<string name="title_reblogged_by">Boostnuto uživatelem</string> <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_2_recipients">%1$s a %2$s</string>
<string name="conversation_more_recipients">%1$s, %2$s a %3$d další</string> <string name="conversation_more_recipients">%1$s, %2$s a %3$d další</string>
<plurals name="max_tab_number_reached"> <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> </plurals>
<string name="description_post_media"> Média %s <string name="description_post_media">Média %s</string>
</string> <string name="description_post_cw">Varování o obsahu: %s</string>
<string name="description_post_cw"> Varování o obsahu: %s <string name="description_post_media_no_description_placeholder">Žádný popis</string>
</string> <string name="description_post_reblogged">Boostnutý</string>
<string name="description_post_media_no_description_placeholder"> Žádný popis <string name="description_post_favourited">Oblíbený</string>
</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_public">Veřejný</string>
<string name="description_visibility_unlisted">Neuvedený</string> <string name="description_visibility_unlisted">Neuvedený</string>
<string name="description_visibility_private">Pro sledující</string> <string name="description_visibility_private">Pro sledující</string>
<string name="description_visibility_direct"> Přímý <string name="description_visibility_direct">Přímý</string>
</string>
<string name="hint_list_name">Název seznamu</string> <string name="hint_list_name">Název seznamu</string>
<string name="edit_hashtag_hint">Hashtag bez #</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="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="notifications_apply_filter">Filtrovat</string>
<string name="filter_apply">Použít</string> <string name="filter_apply">Použít</string>
<string name="pref_title_bot_overlay">Zobrazovat indikátor pro roboty</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="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="action_delete_and_redraft">Smazat a přepsat</string>
<string name="dialog_redraft_post_warning">Smazat a přepsat tento toot\?</string> <string name="dialog_redraft_post_warning">Smazat a přepsat tento příspěvek\?</string>
<string name="poll_info_format"> <!-- 15 hlasů • 1 hodin do konce --> %1$s • %2$s</string> <string name="poll_info_format"> <!-- 15 hlasů • 1 hodina do konce --> %1$s • %2$s</string>
<plurals name="poll_info_votes"> <plurals name="poll_info_votes">
<item quantity="one">%s hlas</item> <item quantity="one">%s hlas</item>
<item quantity="few">%s hlasy</item> <item quantity="few">%s hlasy</item>
@ -403,30 +406,30 @@
<item quantity="few">zbývá %d sekundy</item> <item quantity="few">zbývá %d sekundy</item>
<item quantity="other">zbývá %d sekund</item> <item quantity="other">zbývá %d sekund</item>
</plurals> </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="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="title_domain_mutes">Skryté domény</string>
<string name="action_view_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="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">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="mute_domain_warning_dialog_ok">Skrýt celou doménu</string>
<string name="caption_notoemoji">Aktuální sada emoji od Googlu</string> <string name="caption_notoemoji">Aktuální sada emoji od Googlu</string>
<string name="button_continue">Pokračovat</string> <string name="button_continue">Pokračovat</string>
<string name="button_back">Zpět</string> <string name="button_back">Zpět</string>
<string name="button_done">Hotovo</string> <string name="button_done">Hotovo</string>
<string name="report_sent_success">\@%s úspěšně nahlášen/a</string> <string name="report_sent_success">\@%s byla/a úspěšně nahlášen/a</string>
<string name="hint_additional_info">Dodatečné komentáře</string> <string name="hint_additional_info">Další komentáře</string>
<string name="report_remote_instance">Přeposlat na %s</string> <string name="report_remote_instance">Přeposlat na %s</string>
<string name="failed_report">Nahlášení selhalo</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_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="pref_title_show_notifications_filter">Zobrazit filtr oznámení</string>
<string name="create_poll_title">Anketa</string> <string name="create_poll_title">Anketa</string>
<string name="duration_5_min">5 minut</string> <string name="duration_5_min">5 minut</string>
<string name="duration_30_min">30 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_6_hours">6 hodin</string>
<string name="duration_1_day">1 den</string> <string name="duration_1_day">1 den</string>
<string name="duration_3_days">3 dny</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_allow_multiple_choices">Lze zvolit více možností</string>
<string name="poll_new_choice_hint">Možnost %d</string> <string name="poll_new_choice_hint">Možnost %d</string>
<string name="edit_poll">Upravit</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_edit">Upravit</string>
<string name="action_add_poll">Přidat anketu</string> <string name="action_add_poll">Přidat anketu</string>
<string name="action_access_scheduled_posts">Plánované tooty</string> <string name="action_access_scheduled_posts">Naplánované příspěvky</string>
<string name="action_schedule_post">Naplánovat toot</string> <string name="action_schedule_post">Naplánované příspěvky</string>
<string name="action_reset_schedule">Obnovit</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">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="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> <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="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="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_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="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="list">Seznam</string>
<string name="add_hashtag_title">Přidat hashtag</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="about_powered_by_tusky">Powered by Tusky</string>
<string name="dialog_block_warning">Zablokovat @%s\?</string> <string name="dialog_block_warning">Zablokovat @%s\?</string>
<string name="pref_main_nav_position_option_top">Nahoře</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_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_notifications_desc">Zrušit skrytí oznámení od %s</string>
<string name="action_unmute_desc">Odkrýt %s</string> <string name="action_unmute_desc">Zrušit skrytí %s</string>
<string name="dialog_mute_warning">Ztišit @%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="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="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_announcements">Oznámení</string>
<string name="title_login">Přihlášení</string> <string name="title_login">Přihlášení</string>
<string name="notification_sign_up_format">%s se zaregistroval</string> <string name="notification_sign_up_format">%s se zaregistroval/a</string>
<string name="title_migration_relogin">Přihlaste se znovu pro oznámení</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 stránku přihlášení.</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 poslat!</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="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="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> <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> </resources>

View File

@ -20,7 +20,7 @@
<string name="title_notifications">Hysbysiadau</string> <string name="title_notifications">Hysbysiadau</string>
<string name="title_public_local">Lleol</string> <string name="title_public_local">Lleol</string>
<string name="title_public_federated">Ffedereiddwyd</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">Negeseuon</string>
<string name="title_posts_with_replies">Gydag ymatebion</string> <string name="title_posts_with_replies">Gydag ymatebion</string>
<string name="title_follows">Dilyniadau</string> <string name="title_follows">Dilyniadau</string>
@ -32,7 +32,7 @@
<string name="title_edit_profile">Golygu\'ch proffil</string> <string name="title_edit_profile">Golygu\'ch proffil</string>
<string name="title_drafts">Drafftiau</string> <string name="title_drafts">Drafftiau</string>
<string name="title_licenses">Trwyddedau</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_sensitive_media_title">Cynnwys sensitif</string>
<string name="post_media_hidden_title">Cyfryngau wedi\'u cudd</string> <string name="post_media_hidden_title">Cyfryngau wedi\'u cudd</string>
<string name="post_sensitive_media_directions">Cliciwch i weld</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_more">Chwyddo</string>
<string name="post_content_show_less">Lleihau</string> <string name="post_content_show_less">Lleihau</string>
<string name="footer_empty">Dim byd yma. Tynnwch lawr i adnewyddu!</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_reblog_format">Mae %s wedi hybu\'ch post</string>
<string name="notification_favourite_format">%s wedi hoffi\'ch post</string> <string name="notification_favourite_format">Mae %s wedi hoffi\'ch post</string>
<string name="notification_follow_format">%s wedi\'ch dilyn chi</string> <string name="notification_follow_format">Mae %s wedi\'ch dilyn chi</string>
<string name="report_username_format">Adrodd @%s</string> <string name="report_username_format">Adrodd @%s</string>
<string name="report_comment_hint">Sylwadau ychwanegol?</string> <string name="report_comment_hint">Sylwadau ychwanegol?</string>
<string name="action_quick_reply">Ateb Cyflym</string> <string name="action_quick_reply">Ateb Cyflym</string>
@ -309,7 +309,7 @@
<string name="action_mute_conversation">Tewi sgwrs</string> <string name="action_mute_conversation">Tewi sgwrs</string>
<string name="notifications_apply_filter">Hidlo</string> <string name="notifications_apply_filter">Hidlo</string>
<string name="notification_poll_description">Hysbysiadau am bolau sydd wedi cwblhau</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="no_scheduled_posts">Does gennych ddim negeseuon arfaethedig.</string>
<string name="filter_expiration_format">%s (%s)</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> <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_authorization_denied">Autorisierung wurde abgelehnt.</string>
<string name="error_retrieving_oauth_token">Es konnte kein Login-Token abgerufen werden.</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_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_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_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_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="error_sender_account_gone">Fehler beim Senden des Beitrags.</string>
<string name="title_home">Start</string> <string name="title_home">Start</string>
<string name="title_notifications">Benachrichtigungen</string> <string name="title_notifications">Benachrichtigungen</string>
@ -25,7 +25,7 @@
<string name="title_tab_preferences">Tabs</string> <string name="title_tab_preferences">Tabs</string>
<string name="title_view_thread">Konversation</string> <string name="title_view_thread">Konversation</string>
<string name="title_posts">Beiträge</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_posts_pinned">Angeheftet</string>
<string name="title_follows">Folgt</string> <string name="title_follows">Folgt</string>
<string name="title_followers">Folgende</string> <string name="title_followers">Folgende</string>
@ -43,32 +43,32 @@
<string name="post_sensitive_media_directions">Zum Anzeigen tippen</string> <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_more">Zeige mehr</string>
<string name="post_content_warning_show_less">Zeige weniger</string> <string name="post_content_warning_show_less">Zeige weniger</string>
<string name="post_content_show_more">Mehr</string> <string name="post_content_show_more">Ausklappen</string>
<string name="post_content_show_less">Weniger</string> <string name="post_content_show_less">Einklappen</string>
<string name="message_empty">Hier ist nichts.</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="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_reblog_format">%s teilte deinen Beitrag</string>
<string name="notification_favourite_format">%s favorisierte deinen Beitrag</string> <string name="notification_favourite_format">%s favorisierte deinen Beitrag</string>
<string name="notification_follow_format">%s folgt dir</string> <string name="notification_follow_format">%s folgt dir</string>
<string name="report_username_format">\@%s melden</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_quick_reply">Schnell antworten</string>
<string name="action_reply">Antworten</string> <string name="action_reply">Antworten</string>
<string name="action_reblog">Boosten</string> <string name="action_reblog">Teilen</string>
<string name="action_unreblog">Boost entfernen</string> <string name="action_unreblog">Teilen rückgängig machen</string>
<string name="action_favourite">Favorisieren</string> <string name="action_favourite">Favorisieren</string>
<string name="action_unfavourite">Favorisierung entfernen</string> <string name="action_unfavourite">Favorisierung entfernen</string>
<string name="action_more">Mehr</string> <string name="action_more">Mehr</string>
<string name="action_compose">Beitrag erstellen</string> <string name="action_compose">Beitrag erstellen</string>
<string name="action_login">Mit Mastodon anmelden</string> <string name="action_login">Mit Mastodon anmelden</string>
<string name="action_logout">Ausloggen</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_follow">Folgen</string>
<string name="action_unfollow">Entfolgen</string> <string name="action_unfollow">Entfolgen</string>
<string name="action_block">Blockieren</string> <string name="action_block">Blockieren</string>
<string name="action_unblock">Entblockieren</string> <string name="action_unblock">Entblockieren</string>
<string name="action_hide_reblogs">Geteilte Beiträge verbergen</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_report">Melden</string>
<string name="action_delete">Löschen</string> <string name="action_delete">Löschen</string>
<string name="action_send">TRÖT</string> <string name="action_send">TRÖT</string>
@ -116,12 +116,12 @@
<string name="download_image">%1$s heruntergeladen</string> <string name="download_image">%1$s heruntergeladen</string>
<string name="action_copy_link">Link kopieren</string> <string name="action_copy_link">Link kopieren</string>
<string name="action_open_as">Öffne als %s</string> <string name="action_open_as">Öffne als %s</string>
<string name="action_share_as">Teilen als </string> <string name="action_share_as">Teilen als </string>
<string name="send_post_link_to">Beitragslink teilen…</string> <string name="send_post_link_to">Beitragslink teilen </string>
<string name="send_post_content_to">Beitragsinhalt teilen…</string> <string name="send_post_content_to">Beitragsinhalt teilen </string>
<string name="send_media_to">Mediendatei teilen…</string> <string name="send_media_to">Mediendatei teilen </string>
<string name="confirmation_reported">Gesendet!</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="confirmation_unmuted">Stummschaltung aufgehoben</string>
<string name="post_sent">Gesendet!</string> <string name="post_sent">Gesendet!</string>
<string name="post_sent_long">Antwort erfolgreich 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_content_warning">Inhaltswarnung</string>
<string name="hint_display_name">Anzeigename</string> <string name="hint_display_name">Anzeigename</string>
<string name="hint_note">Über mich</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="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_avatar">Profilbild</string>
<string name="label_header">Titelbild</string> <string name="label_header">Titelbild</string>
<string name="link_whats_an_instance">Was ist eine Instanz?</string> <string name="link_whats_an_instance">Was ist eine Instanz\?</string>
<string name="login_connection">Verbinden </string> <string name="login_connection">Verbinde </string>
<string name="dialog_whats_an_instance">Die Adresse einer Instanz oder Domain kann <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 hier eingegeben werden, wie z.B. mastodon.social, icosahedron.website, social.tchncs.de, und
<a href="https://instances.social">mehr!</a> <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>. \n\nWeitere Informationen gibt es auf <a href="https://joinmastodon.org">joinmastodon.org</a>.
</string> </string>
<string name="dialog_title_finishing_media_upload">Stelle Medienupload fertig</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_download_image">Herunterladen</string>
<string name="dialog_message_cancel_follow_request">Folgeanfrage zurückziehen?</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> <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_filters">Benachrichtigen wenn</string>
<string name="pref_title_notification_filter_mentions">Ich erwähnt werde</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_follows">Mir jemand folgt</string>
<string name="pref_title_notification_filter_reblogs">Jemand meine Posts teilt</string> <string name="pref_title_notification_filter_reblogs">Jemand meine Beiträge teilt</string>
<string name="pref_title_notification_filter_favourites">Jemandem meine Posts gefallen</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_appearance_settings">Aussehen</string>
<string name="pref_title_app_theme">App-Thema</string> <string name="pref_title_app_theme">App-Thema</string>
<string name="pref_title_timelines">Zeitleisten</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="compose_active_account_description">Veröffentlichen als %1$s</string>
<string name="error_failed_set_caption">Fehler beim Speichern der Beschreibung</string> <string name="error_failed_set_caption">Fehler beim Speichern der Beschreibung</string>
<plurals name="hint_describe_for_visually_impaired"> <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> </plurals>
<string name="action_set_caption">Beschreibung eingeben</string> <string name="action_set_caption">Beschreibung eingeben</string>
<string name="action_remove">Entfernen</string> <string name="action_remove">Entfernen</string>
<string name="lock_account_label">Gesperrtes Profil</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="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="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_error_title">Fehler beim Senden</string>
<string name="send_post_notification_channel_name">Beiträge senden</string> <string name="send_post_notification_channel_name">Beiträge senden</string>
<string name="send_post_notification_cancel_title">Senden abgebrochen</string> <string name="send_post_notification_cancel_title">Senden abgebrochen</string>
@ -279,7 +282,7 @@
<string name="emoji_style">Emoji-Stil</string> <string name="emoji_style">Emoji-Stil</string>
<string name="system_default">System-Standard</string> <string name="system_default">System-Standard</string>
<string name="download_fonts">Du musst diese Emoji-Sets zunächst herunterladen</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="expand_collapse_all_posts">Alle Beiträge aus-/einklappen</string>
<string name="action_open_post">Beitrag öffnen</string> <string name="action_open_post">Beitrag öffnen</string>
<string name="restart_required">App-Neustart erforderlich</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_allow_multiple_choices">Mehrere Möglichkeiten</string>
<string name="poll_new_choice_hint">Möglichkeit %d</string> <string name="poll_new_choice_hint">Möglichkeit %d</string>
<string name="title_scheduled_posts">Geplante Beiträge</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_access_scheduled_posts">Geplante Beiträge</string>
<string name="action_schedule_post">Plane Beitrag</string> <string name="action_schedule_post">Plane Beitrag</string>
<string name="action_reset_schedule">Zurücksetzen</string> <string name="action_reset_schedule">Zurücksetzen</string>
@ -430,7 +433,7 @@
<string name="description_post_bookmarked">Als Lesezeichen gespeichert</string> <string name="description_post_bookmarked">Als Lesezeichen gespeichert</string>
<string name="select_list_title">Liste auswählen</string> <string name="select_list_title">Liste auswählen</string>
<string name="list">Liste</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_drafts">Du hast keine Entwürfe.</string>
<string name="no_scheduled_posts">Du hast keine geplanten Beiträge.</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> <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="notification_subscription_name">Neue Beiträge</string>
<string name="pref_title_animate_custom_emojis">GIF-Emojis animieren</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="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="abbreviated_minutes_ago">%d Min.</string>
<string name="review_notifications">Benachrichtigungen überprüfen</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 <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> \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="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_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="limit_notifications">Timeline-Benachrichtigungen einschränken</string>
<string name="action_subscribe_account">Abonnieren</string> <string name="action_subscribe_account">Abonnieren</string>
<string name="action_unsubscribe_account">nicht mehr 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="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="account_date_joined">%1$s beigetreten</string>
<string name="status_count_one_plus">1+</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="error_loading_account_details">Fehler beim Laden der Kontodetails</string>
<string name="action_edit_image">Bild bearbeiten</string> <string name="action_edit_image">Bild bearbeiten</string>
<string name="action_details">Details</string> <string name="action_details">Details</string>
<string name="error_image_edit_failed">Das Bild konnte nicht bearbeitet werden.</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_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_following_hashtag_format">#%s folgen fehlgeschlagen</string>
<string name="error_unfollowing_hashtag_format">#%s entfolgen 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> </resources>

View File

@ -148,11 +148,11 @@
\n \n
\nPliaj informoj troviĝas ĉe <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> \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_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_download_image">Elŝuti</string>
<string name="dialog_message_cancel_follow_request">Nuligi peton de sekvado?</string> <string name="dialog_message_cancel_follow_request">Ĉu nuligi peton de sekvado\?</string>
<string name="dialog_unfollow_warning">Ne plu sekvi?</string> <string name="dialog_unfollow_warning">Ĉu ne plu sekvi\?</string>
<string name="dialog_delete_post_warning">Forigi ĉi tiun mesaĝon?</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_public">Publika: afiŝi en publikaj tempolinioj</string>
<string name="visibility_unlisted">Nelistigita: Ne 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> <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_sound">Sciigi per sono</string>
<string name="pref_title_notification_alert_vibrate">Sciigi per vibro</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_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_mentions">iu mencias min</string>
<string name="pref_title_notification_filter_follows">iu sekvas 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> <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_light">Hela</string>
<string name="app_theme_black">Nigra</string> <string name="app_theme_black">Nigra</string>
<string name="app_theme_auto">Aŭtomata laŭ la horo</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_browser_settings">Retumilo</string>
<string name="pref_title_custom_tabs">Uzi la integritan retumilon</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_language">Lingvo</string>
<string name="pref_title_post_filter">Filtrado de tempolinioj</string> <string name="pref_title_post_filter">Filtrado de tempolinioj</string>
<string name="pref_title_post_tabs">Langetoj</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_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_show_media_preview">Elŝuti antaŭvidojn de aŭdovidaĵoj</string>
<string name="pref_title_proxy_settings">Prokurilo</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_settings">HTTP-prokurilo</string>
<string name="pref_title_http_proxy_enable">Ebligi HTTP prokurilon</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_server">Adreso de HTTP-prokurilo</string>
<string name="pref_title_http_proxy_port">Pordo 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_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_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_public">Publika</string>
<string name="post_privacy_unlisted">Nelistigita</string> <string name="post_privacy_unlisted">Nelistigita</string>
<string name="post_privacy_followers_only">Nur por sekvantoj</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_large">Granda</string>
<string name="post_text_size_largest">La plej granda</string> <string name="post_text_size_largest">La plej granda</string>
<string name="notification_mention_name">Novaj mencioj</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_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_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_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_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_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> <string name="notification_summary_small">%1$s kaj %2$s</string>
<plurals name="notification_title_summary"> <plurals name="notification_title_summary">
<item quantity="one">%d nova interago</item> <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 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. * the url can be changed to link to the localized version of the license.
--> -->
<string name="about_project_site"> Paĝaro de projekto:\n <string name="about_project_site">Paĝaro de la projekto:
https://accelf.net/yuito \n https://accelf.net/yuito</string>
</string> <string name="about_bug_feature_request_site">Raportoj de cimoj kaj petoj de funkcioj:
<string name="about_bug_feature_request_site"> Raportoj de cimo kaj petoj de funkcio:\n \n https://github.com/accelforce/Yuito/issues</string>
https://github.com/accelforce/Yuito/issues
</string>
<string name="about_tusky_account">Profilo de Yuito</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_content">Konigi enhavon de la mesaĝo</string>
<string name="post_share_link">Konigi ligilon al 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="post_media_video">Video</string>
<string name="state_follow_requested">Sekvado petita</string> <string name="state_follow_requested">Sekvado petita</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"--> <!--These are for timestamps on statuses. For example: "16s" or "2d"-->
<string name="abbreviated_in_years">en %dj</string> <string name="abbreviated_in_years">post %dj</string>
<string name="abbreviated_in_days">en %dt</string> <string name="abbreviated_in_days">post %dt</string>
<string name="abbreviated_in_hours">en %dh</string> <string name="abbreviated_in_hours">post %dh</string>
<string name="abbreviated_in_minutes">en %dm</string> <string name="abbreviated_in_minutes">post %dm</string>
<string name="abbreviated_in_seconds">en %ds</string> <string name="abbreviated_in_seconds">post %ds</string>
<string name="abbreviated_years_ago">%dj</string> <string name="abbreviated_years_ago">%dj</string>
<string name="abbreviated_days_ago">%dt</string> <string name="abbreviated_days_ago">%dt</string>
<string name="abbreviated_hours_ago">%dh</string> <string name="abbreviated_hours_ago">%dh</string>
@ -260,15 +258,15 @@
<string name="title_media">Aŭdovidaĵoj</string> <string name="title_media">Aŭdovidaĵoj</string>
<string name="replying_to">Respondo al @%s</string> <string name="replying_to">Respondo al @%s</string>
<string name="load_more_placeholder_text">ŝarĝi pli</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="pref_title_thread_filter_keywords">Konversacioj</string>
<string name="filter_addition_dialog_title">Aldoni filtrilon</string> <string name="filter_addition_dialog_title">Aldoni filtrilon</string>
<string name="filter_edit_dialog_title">Redakti filtrilon</string> <string name="filter_edit_dialog_title">Redakti filtrilon</string>
<string name="filter_dialog_remove_button">Forigi</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="filter_add_description">Frazo filtrota</string>
<string name="add_account_name">Aldoni konton</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="action_lists">Listoj</string>
<string name="title_lists">Listoj</string> <string name="title_lists">Listoj</string>
<string name="error_create_list">Ne povis krei la liston</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_rename_list">Ŝanĝi la nomon de la listo</string>
<string name="action_delete_list">Forigi la liston</string> <string name="action_delete_list">Forigi la liston</string>
<string name="action_edit_list">Redakti 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_add_to_list">Aldoni konton al la listo</string>
<string name="action_remove_from_list">Forigi konton el 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="compose_active_account_description">Afiŝi per konto %1$s</string>
<string name="error_failed_set_caption">Redakto de apudskribo malsukcesis</string> <string name="error_failed_set_caption">Redakto de apudskribo malsukcesis</string>
<plurals name="hint_describe_for_visually_impaired"> <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> </plurals>
<string name="action_set_caption">Redakti apudskribon</string> <string name="action_set_caption">Redakti apudskribon</string>
<string name="action_remove">Forigi</string> <string name="action_remove">Forigi</string>
<string name="lock_account_label">Ŝlosi konton</string> <string name="lock_account_label">Ŝlosi konton</string>
<string name="lock_account_label_description">Vi devas permane rajtigi sekvantojn</string> <string name="lock_account_label_description">Vi devas permane rajtigi sekvantojn</string>
<string name="compose_save_draft">Konservi malneton?</string> <string name="compose_save_draft">Ĉu konservi malneton\?</string>
<string name="send_post_notification_title">Sendante la mesaĝo…</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_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_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="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="action_compose_shortcut">Verki</string>
<string name="error_no_custom_emojis">Via nodo %s ne havas proprajn emoĝiojn</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="emoji_style">Stilo de emoĝioj</string>
<string name="system_default">Sistema valoro</string> <string name="system_default">El la sistemo</string>
<string name="download_fonts">Vi unue devos elŝuti ĉi tiujn emoĝiarojn</string> <string name="download_fonts">Vi unue devos elŝuti ĉi tiun emoĝiaron</string>
<string name="performing_lookup_title">Serĉante</string> <string name="performing_lookup_title">Serĉado</string>
<string name="expand_collapse_all_posts">Pligrandigi/malgrandigi ĉiujn mesaĝojn</string> <string name="expand_collapse_all_posts">Pligrandigi/malgrandigi ĉiujn mesaĝojn</string>
<string name="action_open_post">Malfermi mesaĝon</string> <string name="action_open_post">Malfermi mesaĝon</string>
<string name="restart_required">Restartigo necesas</string> <string name="restart_required">Restartigo necesas</string>
@ -309,15 +309,15 @@
<string name="later">Poste</string> <string name="later">Poste</string>
<string name="restart">Restartigi</string> <string name="restart">Restartigi</string>
<string name="caption_systememoji">Dekomenca emoĝiaro de via aparato</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="caption_twemoji">Norma emoĝiaro de Mastodon</string>
<string name="download_failed">Elŝuto malsukcesis</string> <string name="download_failed">Elŝuto malsukcesis</string>
<string name="profile_badge_bot_text">Roboto</string> <string name="profile_badge_bot_text">Roboto</string>
<string name="account_moved_description">%1$s moviĝis al:</string> <string name="account_moved_description">%1$s moviĝis al:</string>
<string name="reblog_private">Diskonigi al la originala atentaro</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_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_4">CC-BY 4.0</string>
<string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string>
<string name="profile_metadata_label">Profilaj metadatumoj</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_label_label">Etikedo</string>
<string name="profile_metadata_content_label">Enhavo</string> <string name="profile_metadata_content_label">Enhavo</string>
<string name="pref_title_absolute_time">Uzi absolutan tempon</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="unpin_action">Depingli</string>
<string name="pin_action">Alpingli</string> <string name="pin_action">Alpingli</string>
<plurals name="favs"> <plurals name="favs">
@ -337,7 +337,7 @@
<item quantity="other">&lt;b&gt;%s&lt;/b&gt; Diskonigoj</item> <item quantity="other">&lt;b&gt;%s&lt;/b&gt; Diskonigoj</item>
</plurals> </plurals>
<string name="title_reblogged_by">Diskonigita de</string> <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_1_recipients">%1$s</string>
<string name="conversation_2_recipients">%1$s kaj %2$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> <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="description_visibility_direct"> Rekta </string>
<string name="hint_list_name">Nomo de la listo</string> <string name="hint_list_name">Nomo de la listo</string>
<string name="action_delete_and_redraft">Forigi kaj reskribi</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_notification_filter_poll">enketoj finiĝis</string>
<string name="pref_title_bot_overlay">Montri indikilon por robotoj</string> <string name="pref_title_bot_overlay">Montri indikilon pri robotoj</string>
<string name="pref_title_animate_gif_avatars">Moviĝi GIF profilbildojn</string> <string name="pref_title_animate_gif_avatars">Ebligi GIF-profilbildojn</string>
<string name="notification_poll_name">Enketoj</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="edit_hashtag_hint">Kradvorto sen #</string>
<string name="notifications_clear">Viŝi</string> <string name="notifications_clear">Viŝi</string>
<string name="notifications_apply_filter">Filtri</string> <string name="notifications_apply_filter">Filtri</string>
<string name="filter_apply">Apliki</string> <string name="filter_apply">Apliki</string>
<string name="compose_shortcut_long_label">Verki mesaĝon</string> <string name="compose_shortcut_long_label">Verki mesaĝon</string>
<string name="compose_shortcut_short_label">Verki</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="compose_preview_image_description">Agoj por bildo %s</string>
<string name="poll_info_format"> <!-- 15 votes • 1 hour left --> %1$s • %2$s</string> <string name="poll_info_format"> <!-- 15 votes • 1 hour left --> %1$s • %2$s</string>
<plurals name="poll_info_votes"> <plurals name="poll_info_votes">
@ -383,15 +383,15 @@
<item quantity="other">%s voĉdonoj</item> <item quantity="other">%s voĉdonoj</item>
</plurals> </plurals>
<string name="poll_info_time_absolute">finiĝos je %s</string> <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_vote">Voĉdoni</string>
<string name="poll_ended_voted">Enketo al kiu vi voĉdonis finiĝis</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_created">Enketo, kiun vi kreis, finiĝis</string>
<string name="title_domain_mutes">Kaŝitaj domajnoj</string> <string name="title_domain_mutes">Kaŝitaj domajnoj</string>
<string name="action_view_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="action_mute_domain">Silentigi %s</string>
<string name="confirmation_domain_unmuted">%s malsilentigita</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="mute_domain_warning_dialog_ok">Kaŝi la tutan domajnon</string>
<string name="caption_notoemoji">La aktuala emoĝiaro de Google</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> <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="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_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="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">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="title_accounts">Kontoj</string>
<string name="failed_search">Serĉo malsukcesis</string> <string name="failed_search">Serĉo malsukcesis</string>
<string name="action_add_poll">Aldoni baloton</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="pref_title_alway_open_spoiler">Ĉiam montri mesaĝojn kun enhavaj avertoj</string>
<string name="create_poll_title">Baloto</string> <string name="create_poll_title">Enketo</string>
<string name="duration_5_min">5 minutoj</string> <string name="duration_5_min">5 minutoj</string>
<string name="duration_30_min">30 minutoj</string> <string name="duration_30_min">30 minutoj</string>
<string name="duration_1_hour">1 horo</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="description_post_bookmarked">Aldonita al la legosignoj</string>
<string name="select_list_title">Elekti la liston</string> <string name="select_list_title">Elekti la liston</string>
<string name="list">Listo</string> <string name="list">Listo</string>
<string name="post_lookup_error_format">Eraro dum elserĉo de la mesaĝo %s</string> <string name="post_lookup_error_format">Eraro dum serĉo de la mesaĝo %s</string>
<string name="no_drafts">Vi ne havas iun ajn malneton.</string> <string name="no_drafts">Vi havas neniun malneton.</string>
<string name="no_scheduled_posts">Vi ne havas iun ajn planitan mesaĝon.</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="notification_follow_request_name">Petoj de sekvado</string>
<string name="hashtags">Kradvortoj</string> <string name="hashtags">Kradvortoj</string>
<plurals name="poll_info_people"> <plurals name="poll_info_people">
@ -449,8 +449,8 @@
<string name="notification_follow_request_description">Sciigoj pri petoj de sekvado</string> <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="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_hide_notifications">Kaŝi la sciigojn</string>
<string name="dialog_mute_warning">Silentigi @%s\?</string> <string name="dialog_mute_warning">Ĉu silentigi @%s\?</string>
<string name="dialog_block_warning">Bloki @%s\?</string> <string name="dialog_block_warning">Ĉu bloki @%s\?</string>
<string name="action_unmute_conversation">Malsilentigi la konversacion</string> <string name="action_unmute_conversation">Malsilentigi la konversacion</string>
<string name="action_mute_conversation">Silentigi la konversacion</string> <string name="action_mute_conversation">Silentigi la konversacion</string>
<string name="action_unmute_domain">Malsilentigi %s</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="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="warning_scheduling_interval">Mastodon havas minimuman intervalon de planado de 5 minutoj.</string>
<string name="post_media_attachments">Kunsendaĵoj</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="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="action_subscribe_account">Aboni</string>
<string name="draft_deleted">Malneto forigita</string> <string name="draft_deleted">Malneto forigita</string>
<plurals name="error_upload_max_media_reached"> <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="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ŭdovidaĵaj kunsendaĵoj.</item> <item quantity="other">Vi ne povas elŝuti pli ol %1$d aŭdovidaj kunsendaĵoj.</item>
</plurals> </plurals>
<string name="label_duration">Daŭro</string> <string name="label_duration">Daŭro</string>
<string name="duration_indefinite">Nedefinita</string> <string name="duration_indefinite">Nedefinita</string>
<string name="action_unsubscribe_account">Malaboni</string> <string name="action_unsubscribe_account">Malaboni</string>
<string name="notification_subscription_name">Novaj mesaĝoj</string> <string name="notification_subscription_name">Novaj mesaĝoj</string>
<string name="action_unbookmark">Forigi la legosignon</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="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="action_delete_conversation">Forigi konversacion</string>
<string name="notification_subscription_format">%s ĵus afiŝis</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="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="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="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_confirm_favourites">Demandi konfirmon antaŭ ol stelumi</string>
<string name="pref_title_wellbeing_mode">Bonstato</string> <string name="pref_title_wellbeing_mode">Bonstato</string>
<string name="drafts_failed_loading_reply">Ŝarĝado de respondaj informoj malsukcesis</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: <string name="wellbeing_mode_notice">Kelkaj informoj kiuj povas afekci vian mensan bonstaton estos kaŝitaj. Ĉi tiuj inkluzivas:
\n \n
\n - Sciigoj pri stelumo/diskonigo/sekvado \n Sciigoj pri stelumo/diskonigo/sekvado
\n- Nombro de stelumoj/diskonigoj sur la mesaĝoj \n Nombro de stelumoj/diskonigoj sur la mesaĝoj
\n- Statistikoj pri mesaĝoj/sekvantoj sur la profiloj \n Statistikoj pri mesaĝoj/sekvantoj sur la profiloj
\n \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="review_notifications">Kontroli la sciigojn</string>
<string name="limit_notifications">Limigi sciigojn pri tempolinio</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="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_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_name">Novaj kontoj</string>
<string name="notification_sign_up_description">Sciigoj pri novaj uzantoj</string> <string name="notification_sign_up_description">Sciigoj pri novaj uzantoj</string>
<string name="status_count_one_plus">1+</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_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="error_image_edit_failed">La bildo ne povis esti redaktita.</string>
<string name="title_login">Ensaluti</string> <string name="title_login">Ensaluti</string>
@ -540,7 +540,7 @@
<string name="action_dismiss">Fermi</string> <string name="action_dismiss">Fermi</string>
<string name="action_details">Detaloj</string> <string name="action_details">Detaloj</string>
<string name="notification_update_name">Redaktitaj mesaĝoj</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="action_edit_image">Redakti la bildon</string>
<string name="duration_14_days">14 tagoj</string> <string name="duration_14_days">14 tagoj</string>
<string name="duration_30_days">30 tagoj</string> <string name="duration_30_days">30 tagoj</string>
@ -550,8 +550,21 @@
<string name="duration_365_days">365 tagoj</string> <string name="duration_365_days">365 tagoj</string>
<string name="tusky_compose_post_quicksetting_label">Ekverki mesaĝon</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="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="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_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="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> </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_unknown">Ocurrió un error de autorización no identificado.</string>
<string name="error_authorization_denied">La autorización falló.</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_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_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_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_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_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_media_upload_sending">La subida falló.</string>
<string name="error_sender_account_gone">Error al publicar.</string> <string name="error_sender_account_gone">Error al publicar.</string>
<string name="title_home">Inicio</string> <string name="title_home">Inicio</string>
@ -23,7 +23,7 @@
<string name="title_public_federated">Federada</string> <string name="title_public_federated">Federada</string>
<string name="title_direct_messages">Mensajes Directos</string> <string name="title_direct_messages">Mensajes Directos</string>
<string name="title_tab_preferences">Pestañas</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">Estados</string>
<string name="title_posts_with_replies">Con respuestas</string> <string name="title_posts_with_replies">Con respuestas</string>
<string name="title_posts_pinned">Fijado</string> <string name="title_posts_pinned">Fijado</string>
@ -47,8 +47,8 @@
<string name="post_content_show_less">Ocultar</string> <string name="post_content_show_less">Ocultar</string>
<string name="message_empty">Nada aquí.</string> <string name="message_empty">Nada aquí.</string>
<string name="footer_empty">Nada por aquí. ¡Arrastra hacia abajo para recargar!</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_reblog_format">%s impulsó tu publicación</string>
<string name="notification_favourite_format">%s marcó favorito</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="notification_follow_format">%s te siguió</string>
<string name="report_username_format">Reportar @%s</string> <string name="report_username_format">Reportar @%s</string>
<string name="report_comment_hint">¿Información adicional?</string> <string name="report_comment_hint">¿Información adicional?</string>
@ -98,7 +98,7 @@
<string name="action_reject">Rechazar</string> <string name="action_reject">Rechazar</string>
<string name="action_search">Buscar</string> <string name="action_search">Buscar</string>
<string name="action_access_drafts">Borradores</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_content_warning">Aviso de contenido</string>
<string name="action_emoji_keyboard">Teclado de emojis</string> <string name="action_emoji_keyboard">Teclado de emojis</string>
<string name="action_add_tab">Añadir pestaña</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_name">Nuevos seguidores</string>
<string name="notification_follow_description">Notificaciones de nuevos seguidores</string> <string name="notification_follow_description">Notificaciones de nuevos seguidores</string>
<string name="notification_boost_name">Impulsos</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_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_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_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_medium">%1$s, %2$s, y %3$s</string>
<string name="notification_summary_small">%1$s y %2$s</string> <string name="notification_summary_small">%1$s y %2$s</string>
<plurals name="notification_title_summary"> <plurals name="notification_title_summary">
<item quantity="one">%d nueva interacción</item> <item quantity="one">%d nueva interacción</item>
<item quantity="many">%d nuevas interacciones</item>
<item quantity="other">%d nuevas interacciones</item> <item quantity="other">%d nuevas interacciones</item>
</plurals> </plurals>
<string name="description_account_locked">Cuenta protegida</string> <string name="description_account_locked">Cuenta protegida</string>
@ -230,6 +231,7 @@
<string name="post_media_video">Video</string> <string name="post_media_video">Video</string>
<string name="state_follow_requested">Solicitud enviada</string> <string name="state_follow_requested">Solicitud enviada</string>
<!--These are for timestamps on statuses. For example: "16s" or "2d"--> <!--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_years">en %dy</string>
<string name="abbreviated_in_days">en %dd</string> <string name="abbreviated_in_days">en %dd</string>
<string name="abbreviated_in_hours">en %dh</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="add_account_description">Añadir cuenta de Mastodon</string>
<string name="action_lists">Listas</string> <string name="action_lists">Listas</string>
<string name="title_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> <string name="error_failed_set_caption">Error al añadir leyenda</string>
<plurals name="hint_describe_for_visually_impaired"> <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> </plurals>
<string name="action_set_caption">Añadir leyenda</string> <string name="action_set_caption">Añadir leyenda</string>
<string name="action_remove">Eliminar</string> <string name="action_remove">Eliminar</string>
@ -297,12 +304,14 @@
<string name="unpin_action">No fijar</string> <string name="unpin_action">No fijar</string>
<string name="pin_action">Fijar</string> <string name="pin_action">Fijar</string>
<plurals name="favs"> <plurals name="favs">
<item quantity="one">&lt;b&gt;%1$s&lt;/b&gt; Favorito</item> <item quantity="one"><b>%1$s</b> Favorito</item>
<item quantity="other">&lt;b&gt;%1$s&lt;/b&gt; Favoritos</item> <item quantity="many"><b>%1$s</b> Favoritos</item>
<item quantity="other"><b>%1$s</b> Favoritos</item>
</plurals> </plurals>
<plurals name="reblogs"> <plurals name="reblogs">
<item quantity="one"><b>%s</b> impulso</item> <item quantity="one"><b>%s</b> Impulso</item>
<item quantity="other"><b>%s</b> impulsos</item> <item quantity="many"><b>%s</b> Impulsos</item>
<item quantity="other"><b>%s</b> Impulsos</item>
</plurals> </plurals>
<string name="title_reblogged_by">Impulsado por</string> <string name="title_reblogged_by">Impulsado por</string>
<string name="title_favourited_by">Marcado como favorito 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> <string name="conversation_more_recipients">%1$s, %2$s y %3$d más</string>
<plurals name="max_tab_number_reached"> <plurals name="max_tab_number_reached">
<item quantity="one">máximo de %1$d pestaña alcanzada</item> <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> <item quantity="other">máximo de %1$d pestañas alcanzadas</item>
</plurals> </plurals>
<string name="action_mentions">Menciones</string> <string name="action_mentions">Menciones</string>
@ -335,23 +345,28 @@
<string name="poll_vote">Votar</string> <string name="poll_vote">Votar</string>
<plurals name="poll_timespan_days"> <plurals name="poll_timespan_days">
<item quantity="one">%d día restante</item> <item quantity="one">%d día restante</item>
<item quantity="many">%d días restantes</item>
<item quantity="other">%d días restante</item> <item quantity="other">%d días restante</item>
</plurals> </plurals>
<plurals name="poll_timespan_hours"> <plurals name="poll_timespan_hours">
<item quantity="one">%d hora restante</item> <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>
<plurals name="poll_timespan_minutes"> <plurals name="poll_timespan_minutes">
<item quantity="one">%d minuto restante</item> <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>
<plurals name="poll_timespan_seconds"> <plurals name="poll_timespan_seconds">
<item quantity="one">%d segundo restante</item> <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> </plurals>
<string name="poll_info_format"> <!-- 15 votos • queda 1 hora --> %1$s • %2$s</string> <string name="poll_info_format"> <!-- 15 votos • queda 1 hora --> %1$s • %2$s</string>
<plurals name="poll_info_votes"> <plurals name="poll_info_votes">
<item quantity="one">%s voto</item> <item quantity="one">%s voto</item>
<item quantity="many">%s votos</item>
<item quantity="other">%s votos</item> <item quantity="other">%s votos</item>
</plurals> </plurals>
<string name="poll_info_closed">cerrada</string> <string name="poll_info_closed">cerrada</string>
@ -388,7 +403,7 @@
<string name="edit_hashtag_hint">Etiqueta sin #</string> <string name="edit_hashtag_hint">Etiqueta sin #</string>
<string name="notifications_clear">Limpiar</string> <string name="notifications_clear">Limpiar</string>
<string name="notifications_apply_filter">Filtro</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="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="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> <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_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="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_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="title_accounts">Cuentas</string>
<string name="failed_search">Error al buscar</string> <string name="failed_search">Error al buscar</string>
<string name="action_add_poll">Añadir encuesta</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> <string name="pref_title_enable_swipe_for_tabs">Habilitar gesto de deslizar para alternar entre pestañas</string>
<plurals name="poll_info_people"> <plurals name="poll_info_people">
<item quantity="one">%s persona</item> <item quantity="one">%s persona</item>
<item quantity="many">%s personas</item>
<item quantity="other">%s personas</item> <item quantity="other">%s personas</item>
</plurals> </plurals>
<string name="hashtags">Etiquetas</string> <string name="hashtags">Etiquetas</string>
@ -485,20 +501,21 @@
<string name="notification_subscription_format">%s recién publicado</string> <string name="notification_subscription_format">%s recién publicado</string>
<plurals name="error_upload_max_media_reached"> <plurals name="error_upload_max_media_reached">
<item quantity="one">No puedes cargar más de %1$d archivo multimedia adjunto.</item> <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> <item quantity="other">No puedes cargar más de %1$d archivos multimedia adjuntos.</item>
</plurals> </plurals>
<string name="wellbeing_hide_stats_profile">Esconder las estadísticas cuantitativas de los perfiles</string> <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="wellbeing_hide_stats_posts">Esconder las estadísticas cuantitativas de las publicaciones</string>
<string name="review_notifications">Revisar Notificaciones</string> <string name="review_notifications">Revisar Notificaciones</string>
<string name="pref_title_wellbeing_mode">Bienestar</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_description">Notificaciones cuando alguien al que estoy suscrito escribe una publicación</string>
<string name="notification_subscription_name">Nuevos toots</string> <string name="notification_subscription_name">Nuevas publicaciones</string>
<string name="pref_title_notification_filter_subscriptions">alguien al que estoy suscrito publicó un nuevo toot</string> <string name="pref_title_notification_filter_subscriptions">alguien al que estoy suscrito hizo una nueva publicación</string>
<string name="wellbeing_mode_notice">Algunas informaciones que podríam afectar tu bienestar van a ser ocultas. Esto incluye: <string name="wellbeing_mode_notice">Se ocultarán algunas informaciones que podrían afectar a tu bienestar. Esto incluye:
\n \n
\n- Notificaciones de favoritos, impulsos y seguidores \n- Notificaciones de favoritos, impulsos y seguidores
\n- Conteo de favoritos e impulsos en toots \n- Conteo de favoritos e impulsos en publicaciones
\n- Estadísticas de seguidores e toots en perfiles \n- Estadísticas de seguidores y publicaciones en perfiles
\n \n
\nLas notificaciones Push no serán afectadas, pero puedes revisar manualmente tus preferencias.</string> \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> <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_unsubscribe_account">Darse de baja</string>
<string name="action_delete_conversation">Eliminar conversación</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_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> </resources>

View File

@ -227,9 +227,11 @@
<string name="add_account_description">افزودن حساب ماستودون جدید</string> <string name="add_account_description">افزودن حساب ماستودون جدید</string>
<string name="action_lists">فهرست‌ها</string> <string name="action_lists">فهرست‌ها</string>
<string name="title_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> <string name="error_failed_set_caption">شکست در تنظیم عنوان</string>
<plurals name="hint_describe_for_visually_impaired"> <plurals name="hint_describe_for_visually_impaired">
<item quantity="one">توصیف برای کم‌بینایان
\n(کران ۱ نویسه)</item>
<item quantity="other">توصیف برای کم‌بینایان <item quantity="other">توصیف برای کم‌بینایان
\n(کران %d نویسه)</item> \n(کران %d نویسه)</item>
</plurals> </plurals>
@ -340,7 +342,7 @@
<string name="license_cc_by_4">نگارش ۴٫۰ CC-BY</string> <string name="license_cc_by_4">نگارش ۴٫۰ CC-BY</string>
<string name="license_cc_by_sa_4">نگارش ۴٫۰ CC-BY-SA</string> <string name="license_cc_by_sa_4">نگارش ۴٫۰ CC-BY-SA</string>
<plurals name="favs"> <plurals name="favs">
<item quantity="one"><b>%1$s</b> برگزیدن</item> <item quantity="one">۱ برگزیدن</item>
<item quantity="other"><b>%1$s</b> برگزیدن</item> <item quantity="other"><b>%1$s</b> برگزیدن</item>
</plurals> </plurals>
<plurals name="reblogs"> <plurals name="reblogs">
@ -541,4 +543,27 @@
<string name="action_dismiss">رد کردن</string> <string name="action_dismiss">رد کردن</string>
<string name="action_details">جزییات</string> <string name="action_details">جزییات</string>
<string name="saving_draft">ذخیرهٔ پیش‌نویس…</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> </resources>

View File

@ -184,7 +184,7 @@
<string name="error_media_upload_type">Tällaista tiedostoa ei voida ladata ylös.</string> <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="notification_follow_request_format">%s haluaa seurata sinua</string>
<string name="title_public_federated">Verkostoitu</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_username_format">Ilmianna @%s</string>
<string name="report_comment_hint">Lisähuomautuksia\?</string> <string name="report_comment_hint">Lisähuomautuksia\?</string>
<string name="error_loading_account_details">Tilitietojen lataaminen epäonnistui</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="download_image">Ladataan kuvaa %1$s</string>
<string name="error_failed_set_caption">Kuvauksen lisääminen epäonnistui</string> <string name="error_failed_set_caption">Kuvauksen lisääminen epäonnistui</string>
<string name="action_mute_conversation">Mykistä keskustelu</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_logout_confirm">Halutako varmasti kirjautua ulos tililtä %s1\?</string>
<string name="action_unreblog">Poista jako</string> <string name="action_unreblog">Poista jako</string>
<string name="action_create_list">Luo lista</string> <string name="action_create_list">Luo lista</string>
@ -218,7 +218,7 @@
<plurals name="hint_describe_for_visually_impaired"> <plurals name="hint_describe_for_visually_impaired">
<item quantity="one">Kuvaa näkövammaisille <item quantity="one">Kuvaa näkövammaisille
\n(enintään %d merkkiä)</item> \n(enintään %d merkkiä)</item>
<item quantity="other"></item> <item quantity="other"/>
</plurals> </plurals>
<string name="edit_hashtag_hint">Aihetunniste ilman #-merkkiä</string> <string name="edit_hashtag_hint">Aihetunniste ilman #-merkkiä</string>
<string name="title_domain_mutes">Piilotetus verkkonimet</string> <string name="title_domain_mutes">Piilotetus verkkonimet</string>
@ -230,9 +230,9 @@
<string name="action_quick_reply">Vastaa nopeasti</string> <string name="action_quick_reply">Vastaa nopeasti</string>
<string name="action_hide_reblogs">Piilota jaetut julkaisut</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="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="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="error_empty">Tätä kenttää ei voi jättää tyhjäksi.</string>
<string name="title_announcements">Tiedotukset</string> <string name="title_announcements">Tiedotukset</string>
<string name="notification_subscription_description">Ilmoitukset seuraamiesi uusista julkaisuista</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_mute_notifications_desc">Mykistä ilmoitukset tililtä %s</string>
<string name="action_details">Yksityiskohdat</string> <string name="action_details">Yksityiskohdat</string>
<string name="error_image_edit_failed">Kuvaa ei voitu muokata.</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> </resources>

View File

@ -561,4 +561,6 @@
<string name="description_post_language">Langue du message</string> <string name="description_post_language">Langue du message</string>
<string name="duration_no_change">(Aucune modification)</string> <string name="duration_no_change">(Aucune modification)</string>
<string name="url_domain_notifier">%s (🔗 %s)</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> </resources>

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