Merge remote-tracking branch 'tuskyapp/develop'
# Conflicts: # app/build.gradle # app/src/main/java/com/keylesspalace/tusky/MainActivity.kt # app/src/main/java/com/keylesspalace/tusky/entity/Account.kt # app/src/main/res/values-zh-rCN/strings.xml # app/src/main/res/values-zh-rTW/strings.xml # app/src/main/res/values/strings.xml
This commit is contained in:
commit
25bee533f1
|
@ -7,9 +7,13 @@ apply from: "../instance-build.gradle"
|
|||
|
||||
def getGitSha = {
|
||||
def stdout = new ByteArrayOutputStream()
|
||||
exec {
|
||||
commandLine 'git', 'rev-parse', '--short', 'HEAD'
|
||||
standardOutput = stdout
|
||||
try {
|
||||
exec {
|
||||
commandLine 'git', 'rev-parse', '--short', 'HEAD'
|
||||
standardOutput = stdout
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return "unknown"
|
||||
}
|
||||
return stdout.toString().trim()
|
||||
}
|
||||
|
@ -101,7 +105,7 @@ ext.roomVersion = '2.4.2'
|
|||
ext.retrofitVersion = '2.9.0'
|
||||
ext.okhttpVersion = '4.9.3'
|
||||
ext.glideVersion = '4.13.1'
|
||||
ext.daggerVersion = '2.41'
|
||||
ext.daggerVersion = '2.42'
|
||||
ext.materialdrawerVersion = '8.4.5'
|
||||
ext.emoji2_version = '1.1.0'
|
||||
ext.filemojicompat_version = '3.2.1'
|
||||
|
@ -143,7 +147,7 @@ dependencies {
|
|||
kapt "androidx.room:room-compiler:$roomVersion"
|
||||
implementation 'androidx.core:core-splashscreen:1.0.0-beta02'
|
||||
|
||||
implementation "com.google.android.material:material:1.5.0"
|
||||
implementation "com.google.android.material:material:1.6.0"
|
||||
|
||||
implementation "com.google.code.gson:gson:2.9.0"
|
||||
|
||||
|
@ -190,6 +194,9 @@ dependencies {
|
|||
implementation "de.c1710:filemojicompat:$filemojicompat_version"
|
||||
implementation "de.c1710:filemojicompat-defaults:$filemojicompat_version"
|
||||
|
||||
implementation "org.bouncycastle:bcprov-jdk15on:1.70"
|
||||
implementation "com.github.UnifiedPush:android-connector:2.0.0"
|
||||
|
||||
testImplementation "androidx.test.ext:junit:1.1.3"
|
||||
testImplementation "org.robolectric:robolectric:4.4"
|
||||
testImplementation "org.mockito:mockito-inline:4.4.0"
|
||||
|
|
|
@ -0,0 +1,863 @@
|
|||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 36,
|
||||
"identityHash": "92ef93a129d5370539d6a62fd16f53d8",
|
||||
"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)",
|
||||
"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
|
||||
}
|
||||
],
|
||||
"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, `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, `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": "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": "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, 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
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"columnNames": [
|
||||
"instance"
|
||||
],
|
||||
"autoGenerate": false
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "TimelineStatusEntity",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `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, `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": "emojis",
|
||||
"columnName": "emojis",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogsCount",
|
||||
"columnName": "reblogsCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "favouritesCount",
|
||||
"columnName": "favouritesCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "reblogged",
|
||||
"columnName": "reblogged",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "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": "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, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_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, PRIMARY KEY(`id`, `accountId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "accountId",
|
||||
"columnName": "accountId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "accounts",
|
||||
"columnName": "accounts",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "unread",
|
||||
"columnName": "unread",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.id",
|
||||
"columnName": "s_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.url",
|
||||
"columnName": "s_url",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.inReplyToId",
|
||||
"columnName": "s_inReplyToId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.inReplyToAccountId",
|
||||
"columnName": "s_inReplyToAccountId",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.account",
|
||||
"columnName": "s_account",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.content",
|
||||
"columnName": "s_content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.createdAt",
|
||||
"columnName": "s_createdAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.emojis",
|
||||
"columnName": "s_emojis",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.favouritesCount",
|
||||
"columnName": "s_favouritesCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.favourited",
|
||||
"columnName": "s_favourited",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastStatus.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
|
||||
}
|
||||
],
|
||||
"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, '92ef93a129d5370539d6a62fd16f53d8')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
<application
|
||||
android:name=".TuskyApplication"
|
||||
android:appCategory="social"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
|
@ -147,6 +148,29 @@
|
|||
android:name=".receiver.SendStatusBroadcastReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
<receiver
|
||||
android:exported="true"
|
||||
android:enabled="true"
|
||||
android:name=".receiver.UnifiedPushBroadcastReceiver"
|
||||
tools:ignore="ExportedReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="org.unifiedpush.android.connector.MESSAGE"/>
|
||||
<action android:name="org.unifiedpush.android.connector.UNREGISTERED"/>
|
||||
<action android:name="org.unifiedpush.android.connector.NEW_ENDPOINT"/>
|
||||
<action android:name="org.unifiedpush.android.connector.REGISTRATION_FAILED"/>
|
||||
<action android:name="org.unifiedpush.android.connector.REGISTRATION_REFUSED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:exported="true"
|
||||
android:enabled="true"
|
||||
android:name=".receiver.NotificationBlockStateBroadcastReceiver"
|
||||
tools:ignore="ExportedReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.app.action.NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED"/>
|
||||
<action android:name="android.app.action.NOTIFICATION_CHANNEL_GROUP_BLOCK_STATE_CHANGED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<service
|
||||
android:name=".service.TuskyTileService"
|
||||
|
|
|
@ -198,9 +198,9 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
|||
).show()
|
||||
}
|
||||
|
||||
private fun onListSelected(listId: String) {
|
||||
private fun onListSelected(listId: String, listTitle: String) {
|
||||
startActivityWithSlideInAnimation(
|
||||
StatusListActivity.newListIntent(this, listId)
|
||||
StatusListActivity.newListIntent(this, listId, listTitle)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -270,7 +270,8 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
|||
|
||||
override fun onClick(v: View) {
|
||||
if (v == itemView) {
|
||||
onListSelected(getItem(bindingAdapterPosition).id)
|
||||
val list = getItem(bindingAdapterPosition)
|
||||
onListSelected(list.id, list.title)
|
||||
} else {
|
||||
onMore(getItem(bindingAdapterPosition), v)
|
||||
}
|
||||
|
|
|
@ -75,6 +75,10 @@ import com.keylesspalace.tusky.components.drafts.DraftHelper
|
|||
import com.keylesspalace.tusky.components.drafts.DraftsActivity
|
||||
import com.keylesspalace.tusky.components.login.LoginActivity
|
||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
||||
import com.keylesspalace.tusky.components.notifications.disableAllNotifications
|
||||
import com.keylesspalace.tusky.components.notifications.disableUnifiedPushNotificationsForAccount
|
||||
import com.keylesspalace.tusky.components.notifications.enablePushNotificationsWithFallback
|
||||
import com.keylesspalace.tusky.components.notifications.showMigrationNoticeIfNecessary
|
||||
import com.keylesspalace.tusky.components.preference.PreferencesActivity
|
||||
import com.keylesspalace.tusky.components.scheduled.ScheduledStatusActivity
|
||||
import com.keylesspalace.tusky.components.search.SearchActivity
|
||||
|
@ -267,23 +271,10 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
|
||||
setupTabs(showNotificationTab)
|
||||
|
||||
val pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin)
|
||||
binding.viewPager.setPageTransformer(MarginPageTransformer(pageMargin))
|
||||
|
||||
val uswSwipeForTabs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
.getBoolean("enableSwipeForTabs", true)
|
||||
binding.viewPager.isUserInputEnabled = uswSwipeForTabs
|
||||
|
||||
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean(PrefKeys.VIEW_PAGER_OFF_SCREEN_LIMIT, false)) {
|
||||
binding.viewPager.offscreenPageLimit = 9
|
||||
}
|
||||
|
||||
// Setup push notifications
|
||||
if (NotificationHelper.areNotificationsEnabled(this, accountManager)) {
|
||||
NotificationHelper.enablePullNotifications(this)
|
||||
} else {
|
||||
NotificationHelper.disablePullNotifications(this)
|
||||
}
|
||||
eventHub.events
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
|
@ -796,6 +787,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
(adapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect()
|
||||
}
|
||||
|
||||
updateProfiles()
|
||||
|
||||
keepScreenOn()
|
||||
return popups
|
||||
}
|
||||
|
@ -811,7 +804,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
}
|
||||
// open LoginActivity to add new account
|
||||
if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) {
|
||||
startActivityWithSlideInAnimation(LoginActivity.getIntent(this, true))
|
||||
startActivityWithSlideInAnimation(LoginActivity.getIntent(this, LoginActivity.MODE_ADDITIONAL_LOGIN))
|
||||
return false
|
||||
}
|
||||
// change Account
|
||||
|
@ -842,6 +835,10 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
.setMessage(getString(R.string.action_logout_confirm, activeAccount.fullName))
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||
lifecycleScope.launch {
|
||||
// Only disable UnifiedPush for this account -- do not call disableNotifications(),
|
||||
// which unnecessarily disables it for all accounts and then re-enables it again at
|
||||
// the next launch
|
||||
disableUnifiedPushNotificationsForAccount(this@MainActivity, activeAccount)
|
||||
NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, this@MainActivity)
|
||||
cacheUpdater.clearForUser(activeAccount.id)
|
||||
conversationRepository.deleteCacheForAccount(activeAccount.id)
|
||||
|
@ -856,7 +853,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
NotificationHelper.disablePullNotifications(this@MainActivity)
|
||||
}
|
||||
val intent = if (newAccount == null) {
|
||||
LoginActivity.getIntent(this@MainActivity, false)
|
||||
LoginActivity.getIntent(this@MainActivity, LoginActivity.MODE_DEFAULT)
|
||||
} else {
|
||||
Intent(this@MainActivity, MainActivity::class.java)
|
||||
}
|
||||
|
@ -890,6 +887,16 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
accountManager.updateActiveAccount(me)
|
||||
NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this)
|
||||
|
||||
// Setup push notifications
|
||||
showMigrationNoticeIfNecessary(this, binding.root, accountManager)
|
||||
if (NotificationHelper.areNotificationsEnabled(this, accountManager)) {
|
||||
lifecycleScope.launch {
|
||||
enablePushNotificationsWithFallback(this@MainActivity, mastodonApi, accountManager)
|
||||
}
|
||||
} else {
|
||||
disableAllNotifications(this, accountManager)
|
||||
}
|
||||
|
||||
accountLocked = me.locked
|
||||
|
||||
updateProfiles()
|
||||
|
|
|
@ -46,7 +46,7 @@ class SplashActivity : AppCompatActivity(), Injectable {
|
|||
val intent = if (accountManager.activeAccount != null) {
|
||||
Intent(this, MainActivity::class.java)
|
||||
} else {
|
||||
LoginActivity.getIntent(this, false)
|
||||
LoginActivity.getIntent(this, LoginActivity.MODE_DEFAULT)
|
||||
}
|
||||
startActivity(intent)
|
||||
finish()
|
||||
|
|
|
@ -59,7 +59,7 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
Kind.FAVOURITES -> getString(R.string.title_favourites)
|
||||
Kind.BOOKMARKS -> getString(R.string.title_bookmarks)
|
||||
Kind.TAG -> getString(R.string.title_tag).format(hashtag)
|
||||
else -> getString(R.string.title_list_timeline)
|
||||
else -> intent.getStringExtra(EXTRA_LIST_TITLE)
|
||||
}
|
||||
|
||||
supportActionBar?.run {
|
||||
|
@ -94,6 +94,7 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
|
||||
private const val EXTRA_KIND = "kind"
|
||||
private const val EXTRA_LIST_ID = "id"
|
||||
private const val EXTRA_LIST_TITLE = "title"
|
||||
private const val EXTRA_HASHTAG = "tag"
|
||||
|
||||
fun newFavouritesIntent(context: Context) =
|
||||
|
@ -106,10 +107,11 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
putExtra(EXTRA_KIND, Kind.BOOKMARKS.name)
|
||||
}
|
||||
|
||||
fun newListIntent(context: Context, listId: String) =
|
||||
fun newListIntent(context: Context, listId: String, listTitle: String) =
|
||||
Intent(context, StatusListActivity::class.java).apply {
|
||||
putExtra(EXTRA_KIND, Kind.LIST.name)
|
||||
putExtra(EXTRA_LIST_ID, listId)
|
||||
putExtra(EXTRA_LIST_TITLE, listTitle)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
|
|
@ -539,8 +539,13 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
message.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
|
||||
String wholeMessage = String.format(format, displayName);
|
||||
final SpannableStringBuilder str = new SpannableStringBuilder(wholeMessage);
|
||||
str.setSpan(new StyleSpan(Typeface.BOLD), 0, displayName.length(),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
int displayNameIndex = format.indexOf("%s");
|
||||
str.setSpan(
|
||||
new StyleSpan(Typeface.BOLD),
|
||||
displayNameIndex,
|
||||
displayNameIndex + displayName.length(),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
CharSequence emojifiedText = CustomEmojiHelper.emojify(
|
||||
str, notificationViewData.getAccount().getEmojis(), message, statusDisplayOptions.animateEmojis()
|
||||
);
|
||||
|
|
|
@ -84,6 +84,9 @@ import com.keylesspalace.tusky.view.showMuteAccountDialog
|
|||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
import java.text.NumberFormat
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.abs
|
||||
|
||||
|
@ -418,6 +421,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
updateToolbar()
|
||||
updateMovedAccount()
|
||||
updateRemoteAccount()
|
||||
updateAccountJoinedDate()
|
||||
updateAccountStats()
|
||||
invalidateOptionsMenu()
|
||||
|
||||
|
@ -427,6 +431,20 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
}
|
||||
}
|
||||
|
||||
private fun updateAccountJoinedDate() {
|
||||
loadedAccount?.let { account ->
|
||||
try {
|
||||
binding.accountDateJoined.text = resources.getString(
|
||||
R.string.account_date_joined,
|
||||
SimpleDateFormat("MMMM, yyyy", Locale.getDefault()).format(account.createdAt)
|
||||
)
|
||||
binding.accountDateJoined.visibility = View.VISIBLE
|
||||
} catch (e: ParseException) {
|
||||
binding.accountDateJoined.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load account's avatar and header image
|
||||
*/
|
||||
|
|
|
@ -82,7 +82,6 @@ import com.keylesspalace.tusky.entity.Emoji
|
|||
import com.keylesspalace.tusky.entity.NewPoll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.util.ComposeTokenizer
|
||||
import com.keylesspalace.tusky.util.PickMediaFiles
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.afterTextChanged
|
||||
|
@ -361,7 +360,8 @@ class ComposeActivity :
|
|||
ComposeAutoCompleteAdapter(
|
||||
this,
|
||||
preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
|
||||
preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
||||
preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false),
|
||||
preferences.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, true)
|
||||
)
|
||||
)
|
||||
binding.composeEditField.setTokenizer(ComposeTokenizer())
|
||||
|
|
|
@ -1,320 +0,0 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.compose;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.Filter;
|
||||
import android.widget.Filterable;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.entity.Emoji;
|
||||
import com.keylesspalace.tusky.entity.HashTag;
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount;
|
||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Created by charlag on 12/11/17.
|
||||
*/
|
||||
|
||||
public class ComposeAutoCompleteAdapter extends BaseAdapter
|
||||
implements Filterable {
|
||||
private static final int ACCOUNT_VIEW_TYPE = 1;
|
||||
private static final int HASHTAG_VIEW_TYPE = 2;
|
||||
private static final int EMOJI_VIEW_TYPE = 3;
|
||||
private static final int SEPARATOR_VIEW_TYPE = 0;
|
||||
|
||||
private final ArrayList<AutocompleteResult> resultList;
|
||||
private final AutocompletionProvider autocompletionProvider;
|
||||
private final boolean animateAvatar;
|
||||
private final boolean animateEmojis;
|
||||
|
||||
public ComposeAutoCompleteAdapter(AutocompletionProvider autocompletionProvider, boolean animateAvatar, boolean animateEmojis) {
|
||||
super();
|
||||
resultList = new ArrayList<>();
|
||||
this.autocompletionProvider = autocompletionProvider;
|
||||
this.animateAvatar = animateAvatar;
|
||||
this.animateEmojis = animateEmojis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return resultList.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AutocompleteResult getItem(int index) {
|
||||
return resultList.get(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Filter getFilter() {
|
||||
return new Filter() {
|
||||
@Override
|
||||
public CharSequence convertResultToString(Object resultValue) {
|
||||
if (resultValue instanceof AccountResult) {
|
||||
return formatUsername(((AccountResult) resultValue));
|
||||
} else if (resultValue instanceof HashtagResult) {
|
||||
return formatHashtag((HashtagResult) resultValue);
|
||||
} else if (resultValue instanceof EmojiResult) {
|
||||
return formatEmoji((EmojiResult) resultValue);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// This method is invoked in a worker thread.
|
||||
@Override
|
||||
protected FilterResults performFiltering(CharSequence constraint) {
|
||||
FilterResults filterResults = new FilterResults();
|
||||
if (constraint != null) {
|
||||
List<AutocompleteResult> results =
|
||||
autocompletionProvider.search(constraint.toString());
|
||||
filterResults.values = results;
|
||||
filterResults.count = results.size();
|
||||
}
|
||||
return filterResults;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
protected void publishResults(CharSequence constraint, FilterResults results) {
|
||||
if (results != null && results.count > 0) {
|
||||
resultList.clear();
|
||||
resultList.addAll((List<AutocompleteResult>) results.values);
|
||||
notifyDataSetChanged();
|
||||
} else {
|
||||
notifyDataSetInvalidated();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||
View view = convertView;
|
||||
final Context context = parent.getContext();
|
||||
|
||||
switch (getItemViewType(position)) {
|
||||
case ACCOUNT_VIEW_TYPE:
|
||||
AccountViewHolder accountViewHolder;
|
||||
if (convertView == null) {
|
||||
view = ((LayoutInflater) context
|
||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE))
|
||||
.inflate(R.layout.item_autocomplete_account, parent, false);
|
||||
}
|
||||
if (view.getTag() == null) {
|
||||
view.setTag(new AccountViewHolder(view));
|
||||
}
|
||||
accountViewHolder = (AccountViewHolder) view.getTag();
|
||||
|
||||
AccountResult accountResult = ((AccountResult) getItem(position));
|
||||
if (accountResult != null) {
|
||||
TimelineAccount account = accountResult.account;
|
||||
String formattedUsername = context.getString(
|
||||
R.string.post_username_format,
|
||||
account.getUsername()
|
||||
);
|
||||
accountViewHolder.username.setText(formattedUsername);
|
||||
CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(),
|
||||
account.getEmojis(), accountViewHolder.displayName, animateEmojis);
|
||||
accountViewHolder.displayName.setText(emojifiedName);
|
||||
|
||||
int avatarRadius = accountViewHolder.avatar.getContext().getResources()
|
||||
.getDimensionPixelSize(R.dimen.avatar_radius_42dp);
|
||||
|
||||
ImageLoadingHelper.loadAvatar(
|
||||
account.getAvatar(),
|
||||
accountViewHolder.avatar,
|
||||
avatarRadius,
|
||||
animateAvatar
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case HASHTAG_VIEW_TYPE:
|
||||
if (convertView == null) {
|
||||
view = ((LayoutInflater) context
|
||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE))
|
||||
.inflate(R.layout.item_autocomplete_hashtag, parent, false);
|
||||
}
|
||||
|
||||
HashtagResult result = (HashtagResult) getItem(position);
|
||||
if (result != null) {
|
||||
((TextView) view).setText(formatHashtag(result));
|
||||
}
|
||||
break;
|
||||
|
||||
case EMOJI_VIEW_TYPE:
|
||||
EmojiViewHolder emojiViewHolder;
|
||||
if (convertView == null) {
|
||||
view = ((LayoutInflater) context
|
||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE))
|
||||
.inflate(R.layout.item_autocomplete_emoji, parent, false);
|
||||
}
|
||||
if (view.getTag() == null) {
|
||||
view.setTag(new EmojiViewHolder(view));
|
||||
}
|
||||
emojiViewHolder = (EmojiViewHolder) view.getTag();
|
||||
|
||||
EmojiResult emojiResult = ((EmojiResult) getItem(position));
|
||||
if (emojiResult != null) {
|
||||
Emoji emoji = emojiResult.emoji;
|
||||
String formattedShortcode = context.getString(
|
||||
R.string.emoji_shortcode_format,
|
||||
emoji.getShortcode()
|
||||
);
|
||||
emojiViewHolder.shortcode.setText(formattedShortcode);
|
||||
Glide.with(emojiViewHolder.preview)
|
||||
.load(emoji.getUrl())
|
||||
.into(emojiViewHolder.preview);
|
||||
}
|
||||
break;
|
||||
|
||||
case SEPARATOR_VIEW_TYPE:
|
||||
if (convertView == null) {
|
||||
view = ((LayoutInflater) context
|
||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE))
|
||||
.inflate(R.layout.item_autocomplete_divider, parent, false);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError("unknown view type");
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private static String formatUsername(AccountResult result) {
|
||||
return String.format("@%s", result.account.getUsername());
|
||||
}
|
||||
|
||||
private static String formatHashtag(HashtagResult result) {
|
||||
return String.format("#%s", result.hashtag);
|
||||
}
|
||||
|
||||
private static String formatEmoji(EmojiResult result) {
|
||||
return String.format(":%s:", result.emoji.getShortcode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewTypeCount() {
|
||||
return 4;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
AutocompleteResult item = getItem(position);
|
||||
|
||||
if (item instanceof AccountResult) {
|
||||
return ACCOUNT_VIEW_TYPE;
|
||||
} else if (item instanceof HashtagResult) {
|
||||
return HASHTAG_VIEW_TYPE;
|
||||
} else if (item instanceof EmojiResult) {
|
||||
return EMOJI_VIEW_TYPE;
|
||||
} else {
|
||||
return SEPARATOR_VIEW_TYPE;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areAllItemsEnabled() {
|
||||
// there may be separators
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled(int position) {
|
||||
return !(getItem(position) instanceof ResultSeparator);
|
||||
}
|
||||
|
||||
public abstract static class AutocompleteResult {
|
||||
AutocompleteResult() {
|
||||
}
|
||||
}
|
||||
|
||||
public final static class AccountResult extends AutocompleteResult {
|
||||
private final TimelineAccount account;
|
||||
|
||||
public AccountResult(TimelineAccount account) {
|
||||
this.account = account;
|
||||
}
|
||||
}
|
||||
|
||||
public final static class HashtagResult extends AutocompleteResult {
|
||||
private final String hashtag;
|
||||
|
||||
public HashtagResult(HashTag hashtag) {
|
||||
this.hashtag = hashtag.getName();
|
||||
}
|
||||
}
|
||||
|
||||
public final static class EmojiResult extends AutocompleteResult {
|
||||
private final Emoji emoji;
|
||||
|
||||
public EmojiResult(Emoji emoji) {
|
||||
this.emoji = emoji;
|
||||
}
|
||||
}
|
||||
|
||||
public final static class ResultSeparator extends AutocompleteResult {}
|
||||
|
||||
public interface AutocompletionProvider {
|
||||
List<AutocompleteResult> search(String mention);
|
||||
}
|
||||
|
||||
private class AccountViewHolder {
|
||||
final TextView username;
|
||||
final TextView displayName;
|
||||
final ImageView avatar;
|
||||
|
||||
private AccountViewHolder(View view) {
|
||||
username = view.findViewById(R.id.username);
|
||||
displayName = view.findViewById(R.id.display_name);
|
||||
avatar = view.findViewById(R.id.avatar);
|
||||
}
|
||||
}
|
||||
|
||||
private class EmojiViewHolder {
|
||||
final TextView shortcode;
|
||||
final ImageView preview;
|
||||
|
||||
private EmojiViewHolder(View view) {
|
||||
shortcode = view.findViewById(R.id.shortcode);
|
||||
preview = view.findViewById(R.id.preview);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,175 @@
|
|||
/* 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.components.compose
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.Filter
|
||||
import android.widget.Filterable
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.bumptech.glide.Glide
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.databinding.ItemAutocompleteAccountBinding
|
||||
import com.keylesspalace.tusky.databinding.ItemAutocompleteEmojiBinding
|
||||
import com.keylesspalace.tusky.databinding.ItemAutocompleteHashtagBinding
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
|
||||
class ComposeAutoCompleteAdapter(
|
||||
private val autocompletionProvider: AutocompletionProvider,
|
||||
private val animateAvatar: Boolean,
|
||||
private val animateEmojis: Boolean,
|
||||
private val showBotBadge: Boolean
|
||||
) : BaseAdapter(), Filterable {
|
||||
|
||||
private var resultList: List<AutocompleteResult> = emptyList()
|
||||
|
||||
override fun getCount() = resultList.size
|
||||
|
||||
override fun getItem(index: Int): AutocompleteResult {
|
||||
return resultList[index]
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return position.toLong()
|
||||
}
|
||||
|
||||
override fun getFilter(): Filter {
|
||||
return object : Filter() {
|
||||
|
||||
override fun convertResultToString(resultValue: Any): CharSequence {
|
||||
return when (resultValue) {
|
||||
is AutocompleteResult.AccountResult -> formatUsername(resultValue)
|
||||
is AutocompleteResult.HashtagResult -> formatHashtag(resultValue)
|
||||
is AutocompleteResult.EmojiResult -> formatEmoji(resultValue)
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
override fun performFiltering(constraint: CharSequence?): FilterResults {
|
||||
val filterResults = FilterResults()
|
||||
if (constraint != null) {
|
||||
val results = autocompletionProvider.search(constraint.toString())
|
||||
filterResults.values = results
|
||||
filterResults.count = results.size
|
||||
}
|
||||
return filterResults
|
||||
}
|
||||
|
||||
override fun publishResults(constraint: CharSequence?, results: FilterResults) {
|
||||
if (results.count > 0) {
|
||||
resultList = results.values as List<AutocompleteResult>
|
||||
notifyDataSetChanged()
|
||||
} else {
|
||||
notifyDataSetInvalidated()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val itemViewType = getItemViewType(position)
|
||||
val context = parent.context
|
||||
|
||||
val view: View = convertView ?: run {
|
||||
val layoutInflater = LayoutInflater.from(context)
|
||||
val binding = when (itemViewType) {
|
||||
ACCOUNT_VIEW_TYPE -> ItemAutocompleteAccountBinding.inflate(layoutInflater)
|
||||
HASHTAG_VIEW_TYPE -> ItemAutocompleteHashtagBinding.inflate(layoutInflater)
|
||||
EMOJI_VIEW_TYPE -> ItemAutocompleteEmojiBinding.inflate(layoutInflater)
|
||||
else -> throw AssertionError("unknown view type")
|
||||
}
|
||||
binding.root.tag = binding
|
||||
binding.root
|
||||
}
|
||||
|
||||
when (val binding = view.tag) {
|
||||
is ItemAutocompleteAccountBinding -> {
|
||||
val accountResult = getItem(position) as AutocompleteResult.AccountResult
|
||||
val account = accountResult.account
|
||||
binding.username.text = context.getString(R.string.post_username_format, account.username)
|
||||
binding.displayName.text = account.name.emojify(account.emojis, binding.displayName, animateEmojis)
|
||||
val avatarRadius = context.resources.getDimensionPixelSize(R.dimen.avatar_radius_42dp)
|
||||
loadAvatar(
|
||||
account.avatar,
|
||||
binding.avatar,
|
||||
avatarRadius,
|
||||
animateAvatar
|
||||
)
|
||||
binding.avatarBadge.visible(showBotBadge && account.bot)
|
||||
}
|
||||
is ItemAutocompleteHashtagBinding -> {
|
||||
val result = getItem(position) as AutocompleteResult.HashtagResult
|
||||
binding.root.text = formatHashtag(result)
|
||||
}
|
||||
is ItemAutocompleteEmojiBinding -> {
|
||||
val emojiResult = getItem(position) as AutocompleteResult.EmojiResult
|
||||
val (shortcode, url) = emojiResult.emoji
|
||||
binding.shortcode.text = context.getString(R.string.emoji_shortcode_format, shortcode)
|
||||
Glide.with(binding.preview)
|
||||
.load(url)
|
||||
.into(binding.preview)
|
||||
}
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
override fun getViewTypeCount() = 3
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when (getItem(position)) {
|
||||
is AutocompleteResult.AccountResult -> ACCOUNT_VIEW_TYPE
|
||||
is AutocompleteResult.HashtagResult -> HASHTAG_VIEW_TYPE
|
||||
is AutocompleteResult.EmojiResult -> EMOJI_VIEW_TYPE
|
||||
}
|
||||
}
|
||||
|
||||
sealed class AutocompleteResult {
|
||||
class AccountResult(val account: TimelineAccount) : AutocompleteResult()
|
||||
|
||||
class HashtagResult(val hashtag: String) : AutocompleteResult()
|
||||
|
||||
class EmojiResult(val emoji: Emoji) : AutocompleteResult()
|
||||
}
|
||||
|
||||
interface AutocompletionProvider {
|
||||
fun search(token: String): List<AutocompleteResult>
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ACCOUNT_VIEW_TYPE = 0
|
||||
private const val HASHTAG_VIEW_TYPE = 1
|
||||
private const val EMOJI_VIEW_TYPE = 2
|
||||
|
||||
private fun formatUsername(result: AutocompleteResult.AccountResult): String {
|
||||
return String.format("@%s", result.account.username)
|
||||
}
|
||||
|
||||
private fun formatHashtag(result: AutocompleteResult.HashtagResult): String {
|
||||
return String.format("#%s", result.hashtag)
|
||||
}
|
||||
|
||||
private fun formatEmoji(result: AutocompleteResult.EmojiResult): String {
|
||||
return String.format(":%s:", result.emoji.shortcode)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@
|
|||
* 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
|
||||
package com.keylesspalace.tusky.components.compose
|
||||
|
||||
import android.text.SpannableString
|
||||
import android.text.Spanned
|
|
@ -24,6 +24,7 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
||||
import com.keylesspalace.tusky.components.compose.ComposeAutoCompleteAdapter.AutocompleteResult
|
||||
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
||||
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfo
|
||||
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
|
||||
|
@ -38,6 +39,7 @@ import com.keylesspalace.tusky.service.ServiceClient
|
|||
import com.keylesspalace.tusky.service.StatusToSend
|
||||
import com.keylesspalace.tusky.util.combineLiveData
|
||||
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||
import com.keylesspalace.tusky.util.result
|
||||
import com.keylesspalace.tusky.util.toLiveData
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -51,7 +53,6 @@ import kotlinx.coroutines.flow.updateAndGet
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.rx3.rxSingle
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
class ComposeViewModel @Inject constructor(
|
||||
|
@ -195,7 +196,7 @@ class ComposeViewModel @Inject constructor(
|
|||
|
||||
fun removeMediaFromQueue(item: QueuedMedia) {
|
||||
mediaToJob[item.localId]?.cancel()
|
||||
media.update { mediaValue -> mediaValue.filter { it.localId == item.localId } }
|
||||
media.update { mediaValue -> mediaValue.filter { it.localId != item.localId } }
|
||||
}
|
||||
|
||||
fun toggleMarkSensitive() {
|
||||
|
@ -342,48 +343,39 @@ class ComposeViewModel @Inject constructor(
|
|||
return true
|
||||
}
|
||||
|
||||
fun searchAutocompleteSuggestions(token: String): List<ComposeAutoCompleteAdapter.AutocompleteResult> {
|
||||
fun searchAutocompleteSuggestions(token: String): List<AutocompleteResult> {
|
||||
when (token[0]) {
|
||||
'@' -> {
|
||||
return try {
|
||||
api.searchAccounts(query = token.substring(1), limit = 10)
|
||||
.blockingGet()
|
||||
.map { ComposeAutoCompleteAdapter.AccountResult(it) }
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, String.format("Autocomplete search for %s failed.", token), e)
|
||||
emptyList()
|
||||
}
|
||||
return api.searchAccountsCall(query = token.substring(1), limit = 10)
|
||||
.result()
|
||||
.fold({ accounts ->
|
||||
accounts.map { AutocompleteResult.AccountResult(it) }
|
||||
}, { e ->
|
||||
Log.e(TAG, "Autocomplete search for $token failed.", e)
|
||||
emptyList()
|
||||
})
|
||||
}
|
||||
'#' -> {
|
||||
return try {
|
||||
api.searchObservable(query = token, type = SearchType.Hashtag.apiParameter, limit = 10)
|
||||
.blockingGet()
|
||||
.hashtags
|
||||
.map { ComposeAutoCompleteAdapter.HashtagResult(it) }
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, String.format("Autocomplete search for %s failed.", token), e)
|
||||
emptyList()
|
||||
}
|
||||
return api.searchCall(query = token, type = SearchType.Hashtag.apiParameter, limit = 10)
|
||||
.result()
|
||||
.fold({ searchResult ->
|
||||
searchResult.hashtags.map { AutocompleteResult.HashtagResult(it.name) }
|
||||
}, { e ->
|
||||
Log.e(TAG, "Autocomplete search for $token failed.", e)
|
||||
emptyList()
|
||||
})
|
||||
}
|
||||
':' -> {
|
||||
val emojiList = emoji.value ?: return emptyList()
|
||||
val incomplete = token.substring(1)
|
||||
|
||||
val incomplete = token.substring(1).lowercase(Locale.ROOT)
|
||||
val results = ArrayList<ComposeAutoCompleteAdapter.AutocompleteResult>()
|
||||
val resultsInside = ArrayList<ComposeAutoCompleteAdapter.AutocompleteResult>()
|
||||
for (emoji in emojiList) {
|
||||
val shortcode = emoji.shortcode.lowercase(Locale.ROOT)
|
||||
if (shortcode.startsWith(incomplete)) {
|
||||
results.add(ComposeAutoCompleteAdapter.EmojiResult(emoji))
|
||||
} else if (shortcode.indexOf(incomplete, 1) != -1) {
|
||||
resultsInside.add(ComposeAutoCompleteAdapter.EmojiResult(emoji))
|
||||
}
|
||||
return emojiList.filter { emoji ->
|
||||
emoji.shortcode.contains(incomplete, ignoreCase = true)
|
||||
}.sortedBy { emoji ->
|
||||
emoji.shortcode.indexOf(incomplete, ignoreCase = true)
|
||||
}.map { emoji ->
|
||||
AutocompleteResult.EmojiResult(emoji)
|
||||
}
|
||||
if (results.isNotEmpty() && resultsInside.isNotEmpty()) {
|
||||
results.add(ComposeAutoCompleteAdapter.ResultSeparator())
|
||||
}
|
||||
results.addAll(resultsInside)
|
||||
return results
|
||||
}
|
||||
else -> {
|
||||
Log.w(TAG, "Unexpected autocompletion token: $token")
|
||||
|
|
|
@ -90,12 +90,17 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
|
||||
if (savedInstanceState == null &&
|
||||
BuildConfig.CUSTOM_INSTANCE.isNotBlank() &&
|
||||
!isAdditionalLogin()
|
||||
!isAdditionalLogin() && !isAccountMigration()
|
||||
) {
|
||||
binding.domainEditText.setText(BuildConfig.CUSTOM_INSTANCE)
|
||||
binding.domainEditText.setSelection(BuildConfig.CUSTOM_INSTANCE.length)
|
||||
}
|
||||
|
||||
if (isAccountMigration()) {
|
||||
binding.domainEditText.setText(accountManager.activeAccount!!.domain)
|
||||
binding.domainEditText.isEnabled = false
|
||||
}
|
||||
|
||||
if (BuildConfig.CUSTOM_LOGO_URL.isNotBlank()) {
|
||||
Glide.with(binding.loginLogo)
|
||||
.load(BuildConfig.CUSTOM_LOGO_URL)
|
||||
|
@ -118,7 +123,7 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
textView?.movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
|
||||
if (isAdditionalLogin()) {
|
||||
if (isAdditionalLogin() || isAccountMigration()) {
|
||||
setSupportActionBar(binding.toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
supportActionBar?.setDisplayShowTitleEnabled(false)
|
||||
|
@ -133,7 +138,7 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
|
||||
override fun finish() {
|
||||
super.finish()
|
||||
if (isAdditionalLogin()) {
|
||||
if (isAdditionalLogin() || isAccountMigration()) {
|
||||
overridePendingTransition(R.anim.slide_from_left, R.anim.slide_to_right)
|
||||
}
|
||||
}
|
||||
|
@ -223,7 +228,7 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
domain, clientId, clientSecret, oauthRedirectUri, code, "authorization_code"
|
||||
).fold(
|
||||
{ accessToken ->
|
||||
accountManager.addAccount(accessToken.accessToken, domain)
|
||||
accountManager.addAccount(accessToken.accessToken, domain, OAUTH_SCOPES)
|
||||
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
|
@ -255,19 +260,28 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
}
|
||||
|
||||
private fun isAdditionalLogin(): Boolean {
|
||||
return intent.getBooleanExtra(LOGIN_MODE, false)
|
||||
return intent.getIntExtra(LOGIN_MODE, MODE_DEFAULT) == MODE_ADDITIONAL_LOGIN
|
||||
}
|
||||
|
||||
private fun isAccountMigration(): Boolean {
|
||||
return intent.getIntExtra(LOGIN_MODE, MODE_DEFAULT) == MODE_MIGRATION
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "LoginActivity" // logging tag
|
||||
private const val OAUTH_SCOPES = "read write follow"
|
||||
private const val OAUTH_SCOPES = "read write follow push"
|
||||
private const val LOGIN_MODE = "LOGIN_MODE"
|
||||
private const val DOMAIN = "domain"
|
||||
private const val CLIENT_ID = "clientId"
|
||||
private const val CLIENT_SECRET = "clientSecret"
|
||||
|
||||
const val MODE_DEFAULT = 0
|
||||
const val MODE_ADDITIONAL_LOGIN = 1
|
||||
// "Migration" is used to update the OAuth scope granted to the client
|
||||
const val MODE_MIGRATION = 2
|
||||
|
||||
@JvmStatic
|
||||
fun getIntent(context: Context, mode: Boolean): Intent {
|
||||
fun getIntent(context: Context, mode: Int): Intent {
|
||||
val loginIntent = Intent(context, LoginActivity::class.java)
|
||||
loginIntent.putExtra(LOGIN_MODE, mode)
|
||||
return loginIntent
|
||||
|
|
|
@ -57,6 +57,7 @@ import com.keylesspalace.tusky.entity.Notification;
|
|||
import com.keylesspalace.tusky.entity.Poll;
|
||||
import com.keylesspalace.tusky.entity.PollOption;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.network.MastodonApi;
|
||||
import com.keylesspalace.tusky.receiver.NotificationClearBroadcastReceiver;
|
||||
import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver;
|
||||
import com.keylesspalace.tusky.util.StringUtils;
|
||||
|
@ -539,13 +540,18 @@ public class NotificationHelper {
|
|||
}
|
||||
}
|
||||
|
||||
private static boolean filterNotification(AccountEntity account, Notification notification,
|
||||
public static boolean filterNotification(AccountEntity account, Notification notification,
|
||||
Context context) {
|
||||
return filterNotification(account, notification.getType(), context);
|
||||
}
|
||||
|
||||
public static boolean filterNotification(AccountEntity account, Notification.Type type,
|
||||
Context context) {
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
String channelId = getChannelId(account, notification);
|
||||
String channelId = getChannelId(account, type);
|
||||
if(channelId == null) {
|
||||
// unknown notificationtype
|
||||
return false;
|
||||
|
@ -554,7 +560,7 @@ public class NotificationHelper {
|
|||
return channel.getImportance() > NotificationManager.IMPORTANCE_NONE;
|
||||
}
|
||||
|
||||
switch (notification.getType()) {
|
||||
switch (type) {
|
||||
case MENTION:
|
||||
return account.getNotificationsMentioned();
|
||||
case STATUS:
|
||||
|
@ -580,7 +586,12 @@ public class NotificationHelper {
|
|||
|
||||
@Nullable
|
||||
private static String getChannelId(AccountEntity account, Notification notification) {
|
||||
switch (notification.getType()) {
|
||||
return getChannelId(account, notification.getType());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String getChannelId(AccountEntity account, Notification.Type type) {
|
||||
switch (type) {
|
||||
case MENTION:
|
||||
return CHANNEL_MENTION + account.getIdentifier();
|
||||
case STATUS:
|
||||
|
|
|
@ -0,0 +1,220 @@
|
|||
/* 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>. */
|
||||
|
||||
@file:JvmName("PushNotificationHelper")
|
||||
package com.keylesspalace.tusky.components.notifications
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.components.login.LoginActivity
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.CryptoUtil
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.unifiedpush.android.connector.UnifiedPush
|
||||
import retrofit2.HttpException
|
||||
|
||||
private const val TAG = "PushNotificationHelper"
|
||||
|
||||
private const val KEY_MIGRATION_NOTICE_DISMISSED = "migration_notice_dismissed"
|
||||
|
||||
private fun anyAccountNeedsMigration(accountManager: AccountManager): Boolean =
|
||||
accountManager.accounts.any(::accountNeedsMigration)
|
||||
|
||||
private fun accountNeedsMigration(account: AccountEntity): Boolean =
|
||||
!account.oauthScopes.contains("push")
|
||||
|
||||
fun currentAccountNeedsMigration(accountManager: AccountManager): Boolean =
|
||||
accountManager.activeAccount?.let(::accountNeedsMigration) ?: false
|
||||
|
||||
fun showMigrationNoticeIfNecessary(context: Context, parent: View, accountManager: AccountManager) {
|
||||
// No point showing anything if we cannot enable it
|
||||
if (!isUnifiedPushAvailable(context)) return
|
||||
if (!anyAccountNeedsMigration(accountManager)) return
|
||||
|
||||
val pm = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
if (pm.getBoolean(KEY_MIGRATION_NOTICE_DISMISSED, false)) return
|
||||
|
||||
Snackbar.make(parent, R.string.tips_push_notification_migration, Snackbar.LENGTH_INDEFINITE).apply {
|
||||
setAction(R.string.action_details) { showMigrationExplanationDialog(context, accountManager) }
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showMigrationExplanationDialog(context: Context, accountManager: AccountManager) {
|
||||
AlertDialog.Builder(context).apply {
|
||||
if (currentAccountNeedsMigration(accountManager)) {
|
||||
setMessage(R.string.dialog_push_notification_migration)
|
||||
setPositiveButton(R.string.title_migration_relogin) { _, _ ->
|
||||
context.startActivity(LoginActivity.getIntent(context, LoginActivity.MODE_MIGRATION))
|
||||
}
|
||||
} else {
|
||||
setMessage(R.string.dialog_push_notification_migration_other_accounts)
|
||||
}
|
||||
setNegativeButton(R.string.action_dismiss) { dialog, _ ->
|
||||
val pm = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
pm.edit().putBoolean(KEY_MIGRATION_NOTICE_DISMISSED, true).apply()
|
||||
dialog.dismiss()
|
||||
}
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun enableUnifiedPushNotificationsForAccount(context: Context, api: MastodonApi, accountManager: AccountManager, account: AccountEntity) {
|
||||
if (isUnifiedPushNotificationEnabledForAccount(account)) {
|
||||
// Already registered, update the subscription to match notification settings
|
||||
updateUnifiedPushSubscription(context, api, accountManager, account)
|
||||
} else {
|
||||
UnifiedPush.registerAppWithDialog(context, account.id.toString())
|
||||
}
|
||||
}
|
||||
|
||||
fun disableUnifiedPushNotificationsForAccount(context: Context, account: AccountEntity) {
|
||||
if (!isUnifiedPushNotificationEnabledForAccount(account)) {
|
||||
// Not registered
|
||||
return
|
||||
}
|
||||
|
||||
UnifiedPush.unregisterApp(context, account.id.toString())
|
||||
}
|
||||
|
||||
fun isUnifiedPushNotificationEnabledForAccount(account: AccountEntity): Boolean =
|
||||
account.unifiedPushUrl.isNotEmpty()
|
||||
|
||||
private fun isUnifiedPushAvailable(context: Context): Boolean =
|
||||
UnifiedPush.getDistributors(context).isNotEmpty()
|
||||
|
||||
fun canEnablePushNotifications(context: Context, accountManager: AccountManager): Boolean =
|
||||
isUnifiedPushAvailable(context) && !anyAccountNeedsMigration(accountManager)
|
||||
|
||||
suspend fun enablePushNotificationsWithFallback(context: Context, api: MastodonApi, accountManager: AccountManager) {
|
||||
if (!canEnablePushNotifications(context, accountManager)) {
|
||||
// No UP distributors
|
||||
NotificationHelper.enablePullNotifications(context)
|
||||
return
|
||||
}
|
||||
|
||||
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
accountManager.accounts.forEach {
|
||||
val notificationGroupEnabled = Build.VERSION.SDK_INT < 28 ||
|
||||
nm.getNotificationChannelGroup(it.identifier)?.isBlocked == false
|
||||
val shouldEnable = it.notificationsEnabled && notificationGroupEnabled
|
||||
|
||||
if (shouldEnable) {
|
||||
enableUnifiedPushNotificationsForAccount(context, api, accountManager, it)
|
||||
} else {
|
||||
disableUnifiedPushNotificationsForAccount(context, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun disablePushNotifications(context: Context, accountManager: AccountManager) {
|
||||
accountManager.accounts.forEach {
|
||||
disableUnifiedPushNotificationsForAccount(context, it)
|
||||
}
|
||||
}
|
||||
|
||||
fun disableAllNotifications(context: Context, accountManager: AccountManager) {
|
||||
disablePushNotifications(context, accountManager)
|
||||
NotificationHelper.disablePullNotifications(context)
|
||||
}
|
||||
|
||||
private fun buildSubscriptionData(context: Context, account: AccountEntity): Map<String, Boolean> =
|
||||
buildMap {
|
||||
Notification.Type.asList.forEach {
|
||||
put("data[alerts][${it.presentation}]", NotificationHelper.filterNotification(account, it, context))
|
||||
}
|
||||
}
|
||||
|
||||
// Called by UnifiedPush callback
|
||||
suspend fun registerUnifiedPushEndpoint(context: Context, api: MastodonApi, accountManager: AccountManager, account: AccountEntity, endpoint: String) {
|
||||
// Generate a prime256v1 key pair for WebPush
|
||||
// Decryption is unimplemented for now, since Mastodon uses an old WebPush
|
||||
// standard which does not send needed information for decryption in the payload
|
||||
// This makes it not directly compatible with UnifiedPush
|
||||
// As of now, we use it purely as a way to trigger a pull
|
||||
val keyPair = CryptoUtil.generateECKeyPair(CryptoUtil.CURVE_PRIME256_V1)
|
||||
val auth = CryptoUtil.secureRandomBytesEncoded(16)
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
api.subscribePushNotifications(
|
||||
"Bearer ${account.accessToken}", account.domain,
|
||||
endpoint, keyPair.pubkey, auth,
|
||||
buildSubscriptionData(context, account)
|
||||
).onFailure {
|
||||
Log.d(TAG, "Error setting push endpoint for account ${account.id}")
|
||||
Log.d(TAG, Log.getStackTraceString(it))
|
||||
Log.d(TAG, (it as HttpException).response().toString())
|
||||
|
||||
disableUnifiedPushNotificationsForAccount(context, account)
|
||||
}.onSuccess {
|
||||
Log.d(TAG, "UnifiedPush registration succeeded for account ${account.id}")
|
||||
|
||||
account.pushPubKey = keyPair.pubkey
|
||||
account.pushPrivKey = keyPair.privKey
|
||||
account.pushAuth = auth
|
||||
account.pushServerKey = it.serverKey
|
||||
account.unifiedPushUrl = endpoint
|
||||
accountManager.saveAccount(account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Synchronize the enabled / disabled state of notifications with server-side subscription
|
||||
suspend fun updateUnifiedPushSubscription(context: Context, api: MastodonApi, accountManager: AccountManager, account: AccountEntity) {
|
||||
withContext(Dispatchers.IO) {
|
||||
api.updatePushNotificationSubscription(
|
||||
"Bearer ${account.accessToken}", account.domain,
|
||||
buildSubscriptionData(context, account)
|
||||
).onSuccess {
|
||||
Log.d(TAG, "UnifiedPush subscription updated for account ${account.id}")
|
||||
|
||||
account.pushServerKey = it.serverKey
|
||||
accountManager.saveAccount(account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun unregisterUnifiedPushEndpoint(api: MastodonApi, accountManager: AccountManager, account: AccountEntity) {
|
||||
withContext(Dispatchers.IO) {
|
||||
api.unsubscribePushNotifications("Bearer ${account.accessToken}", account.domain)
|
||||
.onFailure {
|
||||
Log.d(TAG, "Error unregistering push endpoint for account " + account.id)
|
||||
Log.d(TAG, Log.getStackTraceString(it))
|
||||
Log.d(TAG, (it as HttpException).response().toString())
|
||||
}
|
||||
.onSuccess {
|
||||
Log.d(TAG, "UnifiedPush unregistration succeeded for account " + account.id)
|
||||
// Clear the URL in database
|
||||
account.unifiedPushUrl = ""
|
||||
account.pushServerKey = ""
|
||||
account.pushAuth = ""
|
||||
account.pushPrivKey = ""
|
||||
account.pushPubKey = ""
|
||||
accountManager.saveAccount(account)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ import androidx.annotation.DrawableRes
|
|||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.AccountListActivity
|
||||
import com.keylesspalace.tusky.BaseActivity
|
||||
import com.keylesspalace.tusky.BuildConfig
|
||||
import com.keylesspalace.tusky.FiltersActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
|
@ -30,6 +31,8 @@ import com.keylesspalace.tusky.TabPreferenceActivity
|
|||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
||||
import com.keylesspalace.tusky.components.login.LoginActivity
|
||||
import com.keylesspalace.tusky.components.notifications.currentAccountNeedsMigration
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
|
@ -139,6 +142,18 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
}
|
||||
}
|
||||
|
||||
if (currentAccountNeedsMigration(accountManager)) {
|
||||
preference {
|
||||
setTitle(R.string.title_migration_relogin)
|
||||
setIcon(R.drawable.ic_logout)
|
||||
setOnPreferenceClickListener {
|
||||
val intent = LoginActivity.getIntent(context, LoginActivity.MODE_MIGRATION)
|
||||
(activity as BaseActivity).startActivityWithSlideInAnimation(intent)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
preferenceCategory(R.string.pref_publishing) {
|
||||
listPreference {
|
||||
setTitle(R.string.pref_default_post_privacy)
|
||||
|
|
|
@ -64,7 +64,15 @@ data class AccountEntity(
|
|||
var activeNotifications: String = "[]",
|
||||
var emojis: List<Emoji> = emptyList(),
|
||||
var tabPreferences: List<TabData> = defaultTabs(),
|
||||
var notificationsFilter: String = "[\"follow_request\"]"
|
||||
var notificationsFilter: String = "[\"follow_request\"]",
|
||||
// Scope cannot be changed without re-login, so store it in case
|
||||
// the scope needs to be changed in the future
|
||||
var oauthScopes: String = "",
|
||||
var unifiedPushUrl: String = "",
|
||||
var pushPubKey: String = "",
|
||||
var pushPrivKey: String = "",
|
||||
var pushAuth: String = "",
|
||||
var pushServerKey: String = "",
|
||||
) {
|
||||
|
||||
val identifier: String
|
||||
|
|
|
@ -54,7 +54,7 @@ class AccountManager @Inject constructor(db: AppDatabase) {
|
|||
* @param accessToken the access token for the new account
|
||||
* @param domain the domain of the accounts Mastodon instance
|
||||
*/
|
||||
fun addAccount(accessToken: String, domain: String) {
|
||||
fun addAccount(accessToken: String, domain: String, oauthScopes: String) {
|
||||
|
||||
activeAccount?.let {
|
||||
it.isActive = false
|
||||
|
@ -65,7 +65,10 @@ class AccountManager @Inject constructor(db: AppDatabase) {
|
|||
|
||||
val maxAccountId = accounts.maxByOrNull { it.id }?.id ?: 0
|
||||
val newAccountId = maxAccountId + 1
|
||||
activeAccount = AccountEntity(id = newAccountId, domain = domain.lowercase(Locale.ROOT), accessToken = accessToken, isActive = true)
|
||||
activeAccount = AccountEntity(
|
||||
id = newAccountId, domain = domain.lowercase(Locale.ROOT),
|
||||
accessToken = accessToken, oauthScopes = oauthScopes, isActive = true
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -189,4 +192,15 @@ class AccountManager @Inject constructor(db: AppDatabase) {
|
|||
id == accountId
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an account by its string identifier
|
||||
* @param identifier the string identifier of the account
|
||||
* @return the requested account or null if it was not found
|
||||
*/
|
||||
fun getAccountByIdentifier(identifier: String): AccountEntity? {
|
||||
return accounts.find {
|
||||
identifier == it.identifier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ import java.io.File;
|
|||
*/
|
||||
@Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
||||
TimelineAccountEntity.class, ConversationEntity.class
|
||||
}, version = 35)
|
||||
}, version = 36)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
|
||||
public abstract AccountDao accountDao();
|
||||
|
@ -542,4 +542,16 @@ public abstract class AppDatabase extends RoomDatabase {
|
|||
database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `card` TEXT");
|
||||
}
|
||||
};
|
||||
|
||||
public static final Migration MIGRATION_35_36 = new Migration(35, 36) {
|
||||
@Override
|
||||
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `oauthScopes` TEXT NOT NULL DEFAULT ''");
|
||||
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `unifiedPushUrl` TEXT NOT NULL DEFAULT ''");
|
||||
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `pushPubKey` TEXT NOT NULL DEFAULT ''");
|
||||
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `pushPrivKey` TEXT NOT NULL DEFAULT ''");
|
||||
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `pushAuth` TEXT NOT NULL DEFAULT ''");
|
||||
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `pushServerKey` TEXT NOT NULL DEFAULT ''");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -70,6 +70,7 @@ class AppModule {
|
|||
AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28, AppDatabase.MIGRATION_28_29,
|
||||
AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31, AppDatabase.MIGRATION_31_32,
|
||||
AppDatabase.MIGRATION_32_33, AppDatabase.MIGRATION_33_34, AppDatabase.MIGRATION_34_35,
|
||||
AppDatabase.MIGRATION_35_36,
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
|
|
@ -16,8 +16,10 @@
|
|||
|
||||
package com.keylesspalace.tusky.di
|
||||
|
||||
import com.keylesspalace.tusky.receiver.NotificationBlockStateBroadcastReceiver
|
||||
import com.keylesspalace.tusky.receiver.NotificationClearBroadcastReceiver
|
||||
import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver
|
||||
import com.keylesspalace.tusky.receiver.UnifiedPushBroadcastReceiver
|
||||
import dagger.Module
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
|
||||
|
@ -28,4 +30,10 @@ abstract class BroadcastReceiverModule {
|
|||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributeNotificationClearBroadcastReceiver(): NotificationClearBroadcastReceiver
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributeUnifiedPushBroadcastReceiver(): UnifiedPushBroadcastReceiver
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract fun contributeNotificationBlockStateBroadcastReceiver(): NotificationBlockStateBroadcastReceiver
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ data class Account(
|
|||
@SerializedName("username") val localUsername: String,
|
||||
@SerializedName("acct", alternate = ["subject"]) val username: String,
|
||||
@SerializedName("display_name") private val displayName: String?, // should never be null per Api definition, but some servers break the contract
|
||||
@SerializedName("created_at") val createdAt: Date,
|
||||
val note: String,
|
||||
val url: String,
|
||||
val avatar: String,
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
/* 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.entity
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class NotificationSubscribeResult(
|
||||
val id: Int,
|
||||
val endpoint: String,
|
||||
@SerializedName("server_key") val serverKey: String,
|
||||
)
|
|
@ -30,6 +30,7 @@ import com.keylesspalace.tusky.entity.MastoList
|
|||
import com.keylesspalace.tusky.entity.MediaUploadResult
|
||||
import com.keylesspalace.tusky.entity.NewStatus
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
import com.keylesspalace.tusky.entity.NotificationSubscribeResult
|
||||
import com.keylesspalace.tusky.entity.Poll
|
||||
import com.keylesspalace.tusky.entity.Relationship
|
||||
import com.keylesspalace.tusky.entity.ScheduledStatus
|
||||
|
@ -47,6 +48,7 @@ import retrofit2.Response
|
|||
import retrofit2.http.Body
|
||||
import retrofit2.http.DELETE
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FieldMap
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.HTTP
|
||||
|
@ -286,6 +288,14 @@ interface MastodonApi {
|
|||
@Query("following") following: Boolean? = null
|
||||
): Single<List<TimelineAccount>>
|
||||
|
||||
@GET("api/v1/accounts/search")
|
||||
fun searchAccountsCall(
|
||||
@Query("q") query: String,
|
||||
@Query("resolve") resolve: Boolean? = null,
|
||||
@Query("limit") limit: Int? = null,
|
||||
@Query("following") following: Boolean? = null
|
||||
): Call<List<TimelineAccount>>
|
||||
|
||||
@GET("api/v1/accounts/{id}")
|
||||
fun account(
|
||||
@Path("id") accountId: String
|
||||
|
@ -591,10 +601,48 @@ interface MastodonApi {
|
|||
@Query("following") following: Boolean? = null
|
||||
): Single<SearchResult>
|
||||
|
||||
@GET("api/v2/search")
|
||||
fun searchCall(
|
||||
@Query("q") query: String?,
|
||||
@Query("type") type: String? = null,
|
||||
@Query("resolve") resolve: Boolean? = null,
|
||||
@Query("limit") limit: Int? = null,
|
||||
@Query("offset") offset: Int? = null,
|
||||
@Query("following") following: Boolean? = null
|
||||
): Call<SearchResult>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/accounts/{id}/note")
|
||||
fun updateAccountNote(
|
||||
@Path("id") accountId: String,
|
||||
@Field("comment") note: String
|
||||
): Single<Relationship>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/push/subscription")
|
||||
suspend fun subscribePushNotifications(
|
||||
@Header("Authorization") auth: String,
|
||||
@Header(DOMAIN_HEADER) domain: String,
|
||||
@Field("subscription[endpoint]") endPoint: String,
|
||||
@Field("subscription[keys][p256dh]") keysP256DH: String,
|
||||
@Field("subscription[keys][auth]") keysAuth: String,
|
||||
// The "data[alerts][]" fields to enable / disable notifications
|
||||
// Should be generated dynamically from all the available notification
|
||||
// types defined in [com.keylesspalace.tusky.entities.Notification.Types]
|
||||
@FieldMap data: Map<String, Boolean>
|
||||
): Result<NotificationSubscribeResult>
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT("api/v1/push/subscription")
|
||||
suspend fun updatePushNotificationSubscription(
|
||||
@Header("Authorization") auth: String,
|
||||
@Header(DOMAIN_HEADER) domain: String,
|
||||
@FieldMap data: Map<String, Boolean>
|
||||
): Result<NotificationSubscribeResult>
|
||||
|
||||
@DELETE("api/v1/push/subscription")
|
||||
suspend fun unsubscribePushNotifications(
|
||||
@Header("Authorization") auth: String,
|
||||
@Header(DOMAIN_HEADER) domain: String,
|
||||
): Result<ResponseBody>
|
||||
}
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/* 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.receiver
|
||||
|
||||
import android.app.NotificationManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import com.keylesspalace.tusky.components.notifications.canEnablePushNotifications
|
||||
import com.keylesspalace.tusky.components.notifications.isUnifiedPushNotificationEnabledForAccount
|
||||
import com.keylesspalace.tusky.components.notifications.updateUnifiedPushSubscription
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import dagger.android.AndroidInjection
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@DelicateCoroutinesApi
|
||||
class NotificationBlockStateBroadcastReceiver : BroadcastReceiver() {
|
||||
@Inject
|
||||
lateinit var mastodonApi: MastodonApi
|
||||
|
||||
@Inject
|
||||
lateinit var accountManager: AccountManager
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
AndroidInjection.inject(this, context)
|
||||
if (Build.VERSION.SDK_INT < 28) return
|
||||
if (!canEnablePushNotifications(context, accountManager)) return
|
||||
|
||||
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
val gid = when (intent.action) {
|
||||
NotificationManager.ACTION_NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED -> {
|
||||
val channelId = intent.getStringExtra(NotificationManager.EXTRA_NOTIFICATION_CHANNEL_ID)
|
||||
nm.getNotificationChannel(channelId).group
|
||||
}
|
||||
NotificationManager.ACTION_NOTIFICATION_CHANNEL_GROUP_BLOCK_STATE_CHANGED -> {
|
||||
intent.getStringExtra(NotificationManager.EXTRA_NOTIFICATION_CHANNEL_GROUP_ID)
|
||||
}
|
||||
else -> null
|
||||
} ?: return
|
||||
|
||||
accountManager.getAccountByIdentifier(gid)?.let { account ->
|
||||
if (isUnifiedPushNotificationEnabledForAccount(account)) {
|
||||
// Update UnifiedPush notification subscription
|
||||
GlobalScope.launch { updateUnifiedPushSubscription(context, mastodonApi, accountManager, account) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/* 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.receiver
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import com.keylesspalace.tusky.components.notifications.NotificationWorker
|
||||
import com.keylesspalace.tusky.components.notifications.registerUnifiedPushEndpoint
|
||||
import com.keylesspalace.tusky.components.notifications.unregisterUnifiedPushEndpoint
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import dagger.android.AndroidInjection
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.unifiedpush.android.connector.MessagingReceiver
|
||||
import javax.inject.Inject
|
||||
|
||||
@DelicateCoroutinesApi
|
||||
class UnifiedPushBroadcastReceiver : MessagingReceiver() {
|
||||
companion object {
|
||||
const val TAG = "UnifiedPush"
|
||||
}
|
||||
|
||||
@Inject
|
||||
lateinit var accountManager: AccountManager
|
||||
|
||||
@Inject
|
||||
lateinit var mastodonApi: MastodonApi
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
super.onReceive(context, intent)
|
||||
AndroidInjection.inject(this, context)
|
||||
}
|
||||
|
||||
override fun onMessage(context: Context, message: ByteArray, instance: String) {
|
||||
AndroidInjection.inject(this, context)
|
||||
Log.d(TAG, "New message received for account $instance")
|
||||
val workManager = WorkManager.getInstance(context)
|
||||
val request = OneTimeWorkRequest.from(NotificationWorker::class.java)
|
||||
workManager.enqueue(request)
|
||||
}
|
||||
|
||||
override fun onNewEndpoint(context: Context, endpoint: String, instance: String) {
|
||||
AndroidInjection.inject(this, context)
|
||||
Log.d(TAG, "Endpoint available for account $instance: $endpoint")
|
||||
accountManager.getAccountById(instance.toLong())?.let {
|
||||
// Launch the coroutine in global scope -- it is short and we don't want to lose the registration event
|
||||
// and there is no saner way to use structured concurrency in a receiver
|
||||
GlobalScope.launch { registerUnifiedPushEndpoint(context, mastodonApi, accountManager, it, endpoint) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRegistrationFailed(context: Context, instance: String) = Unit
|
||||
|
||||
override fun onUnregistered(context: Context, instance: String) {
|
||||
AndroidInjection.inject(this, context)
|
||||
Log.d(TAG, "Endpoint unregistered for account $instance")
|
||||
accountManager.getAccountById(instance.toLong())?.let {
|
||||
// It's fine if the account does not exist anymore -- that means it has been logged out
|
||||
GlobalScope.launch { unregisterUnifiedPushEndpoint(mastodonApi, accountManager, it) }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package com.keylesspalace.tusky.util
|
||||
|
||||
import retrofit2.Call
|
||||
import retrofit2.HttpException
|
||||
|
||||
/**
|
||||
* Synchronously executes the call and returns the response encapsulated in a kotlin.Result.
|
||||
* Since Result is an inline class it is not possible to do this with a Retrofit adapter unfortunately.
|
||||
* More efficient then calling a suspending method with runBlocking
|
||||
*/
|
||||
fun <T> Call<T>.result(): Result<T> {
|
||||
return try {
|
||||
val response = execute()
|
||||
val responseBody = response.body()
|
||||
if (response.isSuccessful && responseBody != null) {
|
||||
Result.success(responseBody)
|
||||
} else {
|
||||
Result.failure(HttpException(response))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
/* 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.Base64
|
||||
import org.bouncycastle.jce.ECNamedCurveTable
|
||||
import org.bouncycastle.jce.interfaces.ECPrivateKey
|
||||
import org.bouncycastle.jce.interfaces.ECPublicKey
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.SecureRandom
|
||||
import java.security.Security
|
||||
|
||||
object CryptoUtil {
|
||||
const val CURVE_PRIME256_V1 = "prime256v1"
|
||||
|
||||
private const val BASE64_FLAGS = Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP
|
||||
|
||||
init {
|
||||
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
|
||||
Security.addProvider(BouncyCastleProvider())
|
||||
}
|
||||
|
||||
private fun secureRandomBytes(len: Int): ByteArray {
|
||||
val ret = ByteArray(len)
|
||||
SecureRandom.getInstance("SHA1PRNG").nextBytes(ret)
|
||||
return ret
|
||||
}
|
||||
|
||||
fun secureRandomBytesEncoded(len: Int): String {
|
||||
return Base64.encodeToString(secureRandomBytes(len), BASE64_FLAGS)
|
||||
}
|
||||
|
||||
data class EncodedKeyPair(val pubkey: String, val privKey: String)
|
||||
|
||||
fun generateECKeyPair(curve: String): EncodedKeyPair {
|
||||
val spec = ECNamedCurveTable.getParameterSpec(curve)
|
||||
val gen = KeyPairGenerator.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME)
|
||||
gen.initialize(spec)
|
||||
val keyPair = gen.genKeyPair()
|
||||
val pubKey = keyPair.public as ECPublicKey
|
||||
val privKey = keyPair.private as ECPrivateKey
|
||||
val encodedPubKey = Base64.encodeToString(pubKey.q.getEncoded(false), BASE64_FLAGS)
|
||||
val encodedPrivKey = Base64.encodeToString(privKey.d.toByteArray(), BASE64_FLAGS)
|
||||
return EncodedKeyPair(encodedPubKey, encodedPrivKey)
|
||||
}
|
||||
}
|
|
@ -98,7 +98,7 @@ public class AccessTokenLoginActivity extends AppCompatActivity implements Injec
|
|||
}
|
||||
|
||||
private void authSucceeded(String domain, String accessToken) {
|
||||
accountManager.addAccount(accessToken, domain);
|
||||
accountManager.addAccount(accessToken, domain, "");
|
||||
log("Completed. Enjoy!");
|
||||
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
|
|
|
@ -235,6 +235,19 @@
|
|||
tools:itemCount="2"
|
||||
tools:listitem="@layout/item_account_field" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/accountDateJoined"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
tools:text="April, 1971"
|
||||
android:textColor="@color/textColorSecondary"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/accountFieldList"
|
||||
app:layout_constraintBottom_toTopOf="@id/accountRemoveView"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/accountRemoveView"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -245,7 +258,7 @@
|
|||
android:lineSpacingMultiplier="1.1"
|
||||
android:text="@string/label_remote_account"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/accountFieldList"
|
||||
app:layout_constraintTop_toBottomOf="@id/accountDateJoined"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
|
|
@ -16,8 +16,8 @@
|
|||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:contentInsetStartWithNavigation="0dp"
|
||||
android:elevation="@dimen/actionbar_elevation"
|
||||
app:contentInsetStartWithNavigation="0dp"
|
||||
app:layout_scrollFlags="scroll|snap|enterAlways"
|
||||
app:navigationIcon="?attr/homeAsUpIndicator" />
|
||||
|
||||
|
@ -27,8 +27,9 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:tabGravity="fill"
|
||||
app:tabMaxWidth="0dp"
|
||||
app:tabMode="fixed"
|
||||
app:tabTextAppearance="@style/TuskyTabAppearance"/>
|
||||
app:tabTextAppearance="@style/TuskyTabAppearance" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
|
@ -38,6 +39,6 @@
|
|||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
<include layout="@layout/item_status_bottom_sheet"/>
|
||||
<include layout="@layout/item_status_bottom_sheet" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -1,48 +1,65 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<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="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="8dp">
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/avatar"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:contentDescription="@null"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:foregroundGravity="center_vertical"
|
||||
android:importantForAccessibility="no"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/avatar_default" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_toEndOf="@id/avatar"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="vertical">
|
||||
<ImageView
|
||||
android:id="@+id/avatarBadge"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:contentDescription="@string/profile_badge_bot_text"
|
||||
android:src="@drawable/bot_badge"
|
||||
app:layout_constraintBottom_toBottomOf="@id/avatar"
|
||||
app:layout_constraintEnd_toEndOf="@id/avatar" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/display_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="?attr/status_text_large"
|
||||
android:textStyle="normal|bold"
|
||||
tools:text="Conny Duck" />
|
||||
<TextView
|
||||
android:id="@+id/displayName"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="14dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="?attr/status_text_large"
|
||||
android:textStyle="normal|bold"
|
||||
app:layout_constraintBottom_toTopOf="@id/username"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/avatar"
|
||||
app:layout_constraintTop_toTopOf="@id/avatar"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="Display name" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/username"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
tools:text="\@ConnyDuck" />
|
||||
<TextView
|
||||
android:id="@+id/username"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="14dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
app:layout_constraintBottom_toBottomOf="@id/avatar"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/avatar"
|
||||
app:layout_constraintTop_toBottomOf="@id/displayName"
|
||||
tools:text="\@username" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<View xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/status_divider" />
|
|
@ -5,24 +5,24 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp">
|
||||
tools:ignore="UseCompoundDrawables">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/preview"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:contentDescription="@null"
|
||||
android:padding="4dp" />
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:importantForAccessibility="no" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/shortcode"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView 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:id="@+id/hashtag"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="12dp"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
android:textStyle="normal|bold"
|
||||
app:drawableStartCompat="@drawable/ic_list"
|
||||
app:drawableTint="?attr/iconColor" />
|
||||
tools:text="#Tusky" />
|
||||
|
|
|
@ -269,7 +269,6 @@
|
|||
<string name="add_account_description">إضافة حساب ماستدون جديد</string>
|
||||
<string name="action_lists">القوائم</string>
|
||||
<string name="title_lists">القوائم</string>
|
||||
<string name="title_list_timeline">الخط الزمني للقائمة</string>
|
||||
<string name="error_create_list">لا يمكن إنشاء قائمة</string>
|
||||
<string name="error_rename_list">لا يمكن إعادة تسمية القائمة</string>
|
||||
<string name="error_delete_list">لا يمكن حذف القائمة</string>
|
||||
|
|
|
@ -151,7 +151,6 @@
|
|||
<string name="error_delete_list">Списъкът не можа да се изтрие</string>
|
||||
<string name="error_create_list">Списъкът не можа да се създаде</string>
|
||||
<string name="error_rename_list">Списъкът не можа да се преименува</string>
|
||||
<string name="title_list_timeline">Списъчна емисия</string>
|
||||
<string name="title_lists">Списъци</string>
|
||||
<string name="action_lists">Списъци</string>
|
||||
<string name="add_account_description">Добавяне на нов Mastodon акаунт</string>
|
||||
|
|
|
@ -75,7 +75,6 @@
|
|||
<string name="error_delete_list">তালিকা মুছে ফেলা যায়নি</string>
|
||||
<string name="error_rename_list">তালিকা নামকরণ করা যায়নি</string>
|
||||
<string name="error_create_list">তালিকা তৈরি করা যায়নি</string>
|
||||
<string name="title_list_timeline">তালিকা টাইমলাইনে রাখুন</string>
|
||||
<string name="title_lists">তালিকাসমূহ</string>
|
||||
<string name="action_lists">তালিকাসমূহ</string>
|
||||
<string name="add_account_description">নতুন ম্যাস্টোডোন অ্যাকাউন্ট যোগ করুন</string>
|
||||
|
|
|
@ -275,7 +275,6 @@
|
|||
<string name="add_account_description">নতুন ম্যাস্টোডোন অ্যাকাউন্ট যোগ করুন</string>
|
||||
<string name="action_lists">তালিকাসমূহ</string>
|
||||
<string name="title_lists">তালিকাসমূহ</string>
|
||||
<string name="title_list_timeline">তালিকা টাইমলাইনে রাখুন</string>
|
||||
<string name="error_create_list">তালিকা তৈরি করা যায়নি</string>
|
||||
<string name="error_rename_list">তালিকা নামকরণ করা যায়নি</string>
|
||||
<string name="error_delete_list">তালিকা মুছে ফেলা যায়নি</string>
|
||||
|
|
|
@ -275,7 +275,6 @@
|
|||
<string name="add_account_description">Afegir un compte de Mastodont</string>
|
||||
<string name="action_lists">Llistes</string>
|
||||
<string name="title_lists">Llistes</string>
|
||||
<string name="title_list_timeline">Cronologia de la llista</string>
|
||||
<string name="error_create_list">És impossible crear la llista</string>
|
||||
<string name="error_rename_list">Impossible reanomenar la llista</string>
|
||||
<string name="error_delete_list">És impossible suprimir la llista</string>
|
||||
|
|
|
@ -403,7 +403,6 @@
|
|||
<string name="error_delete_list">نەیتوانی لیستەکە بسڕێتەوە</string>
|
||||
<string name="error_rename_list">نەیتوانی ناوی لیست بنووسرێ</string>
|
||||
<string name="error_create_list">نەیتوانی لیست دروست بکات</string>
|
||||
<string name="title_list_timeline">لیستی تایم لاین</string>
|
||||
<string name="title_lists">لیستەکان</string>
|
||||
<string name="action_lists">لیستەکان</string>
|
||||
<string name="add_account_description">زیادکردنی ئەژمێری ماتۆدۆنی نوێ</string>
|
||||
|
|
|
@ -274,7 +274,6 @@
|
|||
<string name="add_account_description">Přidat nový účet Mastodon</string>
|
||||
<string name="action_lists">Seznamy</string>
|
||||
<string name="title_lists">Seznamy</string>
|
||||
<string name="title_list_timeline">Časová osa seznamu</string>
|
||||
<string name="error_create_list">Nelze vytvořit seznam</string>
|
||||
<string name="error_rename_list">Nelze přejmenovat seznam</string>
|
||||
<string name="error_delete_list">Nelze smazat seznam</string>
|
||||
|
|
|
@ -236,7 +236,6 @@
|
|||
<string name="add_account_description">Ychwanegu cyfrif Mastodon newydd</string>
|
||||
<string name="action_lists">Rhestri</string>
|
||||
<string name="title_lists">Rhestri</string>
|
||||
<string name="title_list_timeline">Amserlen rhestri</string>
|
||||
<string name="compose_active_account_description">Yn postio â chyfrif %1$s</string>
|
||||
<string name="error_failed_set_caption">Methu gosod pennawd</string>
|
||||
<string name="action_set_caption">Pennu pennawd</string>
|
||||
|
|
|
@ -256,7 +256,6 @@
|
|||
<string name="add_account_description">Neues Mastodon-Konto hinzufügen</string>
|
||||
<string name="action_lists">Listen</string>
|
||||
<string name="title_lists">Listen</string>
|
||||
<string name="title_list_timeline">Liste</string>
|
||||
<string name="action_create_list">Liste erstellen</string>
|
||||
<string name="action_rename_list">Liste umbenennen</string>
|
||||
<string name="action_delete_list">Liste löschen</string>
|
||||
|
|
|
@ -271,7 +271,6 @@
|
|||
<string name="add_account_description">Aldoni novan Mastodon konton</string>
|
||||
<string name="action_lists">Listoj</string>
|
||||
<string name="title_lists">Listoj</string>
|
||||
<string name="title_list_timeline">Tempolinio de la listo</string>
|
||||
<string name="error_create_list">Ne povis krei la liston</string>
|
||||
<string name="error_rename_list">Ne povis ŝanĝi la nomon de la listo</string>
|
||||
<string name="error_delete_list">Ne povis forigi la liston</string>
|
||||
|
|
|
@ -251,7 +251,6 @@
|
|||
<string name="add_account_description">Añadir cuenta de Mastodon</string>
|
||||
<string name="action_lists">Listas</string>
|
||||
<string name="title_lists">Listas</string>
|
||||
<string name="title_list_timeline">Cronología de lista</string>
|
||||
<string name="compose_active_account_description">Publicando con la cuenta %1$s</string>
|
||||
<string name="error_failed_set_caption">Error al añadir leyenda</string>
|
||||
<plurals name="hint_describe_for_visually_impaired">
|
||||
|
|
|
@ -235,7 +235,6 @@
|
|||
<string name="add_account_description">Mastodon kontua gehitu</string>
|
||||
<string name="action_lists">Zerrendak</string>
|
||||
<string name="title_lists">Zerrendak</string>
|
||||
<string name="title_list_timeline">Zerrenda denbora-lerroa</string>
|
||||
<string name="compose_active_account_description">%1$s kontuarekin tut egiten</string>
|
||||
<string name="error_failed_set_caption">Akatsa deskribapena eranstean</string>
|
||||
<plurals name="hint_describe_for_visually_impaired">
|
||||
|
|
|
@ -229,7 +229,6 @@
|
|||
<string name="add_account_description">افزودن حساب ماستودون جدید</string>
|
||||
<string name="action_lists">فهرستها</string>
|
||||
<string name="title_lists">فهرستها</string>
|
||||
<string name="title_list_timeline">خط زمانی فهرست</string>
|
||||
<string name="compose_active_account_description">در حال فرستادن با حساب %1$s</string>
|
||||
<string name="error_failed_set_caption">شکست در تنظیم عنوان</string>
|
||||
<plurals name="hint_describe_for_visually_impaired">
|
||||
|
|
|
@ -275,7 +275,6 @@
|
|||
<string name="add_account_description">Ajouter un nouveau compte Mastodon</string>
|
||||
<string name="action_lists">Listes</string>
|
||||
<string name="title_lists">Listes</string>
|
||||
<string name="title_list_timeline">Fil de la liste</string>
|
||||
<string name="error_create_list">Impossible de créer la liste</string>
|
||||
<string name="error_rename_list">Impossible de renommer la liste</string>
|
||||
<string name="error_delete_list">Impossible de supprimer la liste</string>
|
||||
|
@ -295,7 +294,7 @@
|
|||
<string name="action_set_caption">Mettre une légende</string>
|
||||
<string name="action_remove">Supprimer le média</string>
|
||||
<string name="lock_account_label">Verrouiller le compte</string>
|
||||
<string name="lock_account_label_description">Vous devez approuvez manuellement les abonnements</string>
|
||||
<string name="lock_account_label_description">Vous devez approuver manuellement les abonnements</string>
|
||||
<string name="compose_save_draft">Enregistrer comme brouillon ?</string>
|
||||
<string name="send_post_notification_title">Envoi du pouet…</string>
|
||||
<string name="send_post_notification_error_title">Erreur lors de l’envoi du pouet</string>
|
||||
|
|
|
@ -310,7 +310,6 @@
|
|||
<string name="failed_fetch_posts">Theip ar stádas a fháil</string>
|
||||
<string name="report_description_1">Seolfar an tuarascáil chuig do mhodhnóir freastalaí. Féadfaidh tú míniú a thabhairt ar an bhfáth go bhfuil tú ag tuairisciú an chuntais seo thíos:</string>
|
||||
<string name="add_account_description">Cuir Cuntas Mastodon nua leis</string>
|
||||
<string name="title_list_timeline">Liostaigh amlíne</string>
|
||||
<string name="error_create_list">Níorbh fhéidir liosta a chruthú</string>
|
||||
<string name="error_rename_list">Níorbh fhéidir an liosta a athainmniú</string>
|
||||
<string name="error_delete_list">Níorbh fhéidir an liosta a scriosadh</string>
|
||||
|
|
|
@ -92,7 +92,7 @@
|
|||
<string name="notification_subscription_description">Brathan nuair a dh’fhoillsich cuideigin air a rinn mi fo-sgrìobhadh post ùr</string>
|
||||
<string name="notification_subscription_name">Postaichean ùra</string>
|
||||
<string name="pref_title_notification_filter_subscriptions">dh’fhoillsich cuideigin air a rinn mi fo-sgrìobhadh post ùr</string>
|
||||
<string name="notification_subscription_format">Tha %s air rud a phostadh</string>
|
||||
<string name="notification_subscription_format">Phostaich %s rud</string>
|
||||
<string name="no_announcements">Chan eil brath-fios ann.</string>
|
||||
<string name="title_announcements">Brathan-fios</string>
|
||||
<string name="account_note_saved">Chaidh a shàbhaladh!</string>
|
||||
|
@ -270,7 +270,6 @@
|
|||
<string name="error_delete_list">Cha b’ urrainn dhuinn an liosta a sguabadh às</string>
|
||||
<string name="error_rename_list">Cha b’ urrainn dhut ainm ùr a thoirt air an liosta</string>
|
||||
<string name="error_create_list">Cha b’ urrainn dhuinn an liosta a chruthachadh</string>
|
||||
<string name="title_list_timeline">Loidhne-ama na liosta</string>
|
||||
<string name="add_account_description">Cuir cunntas Mastodon ùr ris</string>
|
||||
<string name="add_account_name">Cuir cunntas ris</string>
|
||||
<string name="filter_add_description">An abairt ri chriathradh</string>
|
||||
|
@ -295,7 +294,7 @@
|
|||
<string name="action_schedule_post">Cuir post air an sgeideal</string>
|
||||
<string name="action_toggle_visibility">Faicsinneachd a’ phuist</string>
|
||||
<string name="action_access_scheduled_posts">Postaichean air an sgeideal</string>
|
||||
<string name="notification_favourite_format">Chuir %s am post agad ris na h-annsachdan</string>
|
||||
<string name="notification_favourite_format">Is annsa le %s am post agad</string>
|
||||
<string name="notification_reblog_format">Bhrosnaich %s am post agad</string>
|
||||
<string name="title_scheduled_posts">Postaichean air an sgeideal</string>
|
||||
<string name="title_view_thread">Snàithlean</string>
|
||||
|
@ -556,4 +555,5 @@
|
|||
<string name="pref_title_notification_filter_updates">chaidh post a rinn mi conaltradh leis a deasachadh</string>
|
||||
<string name="title_login">Clàraich a-steach</string>
|
||||
<string name="error_could_not_load_login_page">Cha b’ urrainn dhuinn duilleag a’ chlàraidh a-steach fhosgladh.</string>
|
||||
<string name="saving_draft">A’ sàbhaladh na dreuchd…</string>
|
||||
</resources>
|
|
@ -275,7 +275,6 @@
|
|||
<string name="error_delete_list">Non se puido eliminar a listaxe</string>
|
||||
<string name="error_rename_list">Non se puido renomear a listaxe</string>
|
||||
<string name="error_create_list">Non se puido crear a listaxe</string>
|
||||
<string name="title_list_timeline">Cronoloxía da listaxe</string>
|
||||
<string name="title_lists">Listaxes</string>
|
||||
<string name="action_lists">Listaxes</string>
|
||||
<string name="add_account_description">Engadir unha nova conta Mastodon</string>
|
||||
|
|
|
@ -248,7 +248,6 @@
|
|||
<string name="compose_save_draft">लिखने को सुरक्षित करें\?</string>
|
||||
<string name="lock_account_label">खाता लॉक करें</string>
|
||||
<string name="action_set_caption">कैप्शन सेट करें</string>
|
||||
<string name="title_list_timeline">सूची टाइमलाइन</string>
|
||||
<string name="add_account_name">खाता जोड़ो</string>
|
||||
<string name="filter_dialog_whole_word">पूरा शब्द</string>
|
||||
<string name="filter_edit_dialog_title">फ़िल्टर संपादित करें</string>
|
||||
|
|
|
@ -336,7 +336,6 @@
|
|||
<string name="abbreviated_in_seconds">%dmp múlva</string>
|
||||
<string name="filter_dialog_whole_word">Teljes szó</string>
|
||||
<string name="filter_dialog_whole_word_description">Ha a kulcsszó csak alfanumerikus karakterekből áll, csak teljes szóra fog illeszkedni</string>
|
||||
<string name="title_list_timeline">Lista idővonal</string>
|
||||
<string name="hint_search_people_list">Általad követettek keresése</string>
|
||||
<string name="action_add_to_list">Fiók hozzáadása a listához</string>
|
||||
<string name="action_remove_from_list">Fiók eltávolítása a listából</string>
|
||||
|
|
|
@ -288,7 +288,6 @@
|
|||
<string name="filter_add_description">Frasi sem á að sía</string>
|
||||
<string name="add_account_name">Bæta við aðgang</string>
|
||||
<string name="add_account_description">Bæta við nýjum Mastodon-aðgangi</string>
|
||||
<string name="title_list_timeline">Lista upp tímalínu</string>
|
||||
<string name="error_create_list">Ekki tókst að búa til lista</string>
|
||||
<string name="error_rename_list">Ekki tókst að endurnefna lista</string>
|
||||
<string name="error_delete_list">Ekki tókst að eyða lista</string>
|
||||
|
|
|
@ -269,7 +269,6 @@
|
|||
<string name="add_account_description">Aggiungi un nuovo Account Mastodon</string>
|
||||
<string name="action_lists">Liste</string>
|
||||
<string name="title_lists">Liste</string>
|
||||
<string name="title_list_timeline">Timeline della lista</string>
|
||||
<string name="error_create_list">Non è stato possibile creare la lista</string>
|
||||
<string name="error_rename_list">Non è stato possibile rinominare la lista</string>
|
||||
<string name="error_delete_list">Non è stato possibile eliminare la lista</string>
|
||||
|
|
|
@ -264,7 +264,6 @@
|
|||
<string name="add_account_description">新しいMastodonアカウントを追加</string>
|
||||
<string name="action_lists">リスト</string>
|
||||
<string name="title_lists">リスト</string>
|
||||
<string name="title_list_timeline">リストタイムライン</string>
|
||||
<string name="error_rename_list">リスト名を変更できませんでした</string>
|
||||
<string name="action_rename_list">リスト名の変更</string>
|
||||
<string name="compose_active_account_description">%1$sで投稿</string>
|
||||
|
|
|
@ -282,7 +282,6 @@
|
|||
<string name="add_account_description">마스토돈 계정을 추가합니다</string>
|
||||
<string name="action_lists">리스트</string>
|
||||
<string name="title_lists">리스트</string>
|
||||
<string name="title_list_timeline">리스트 타임라인</string>
|
||||
<string name="error_create_list">리스트를 만들 수 없습니다.</string>
|
||||
<string name="error_rename_list">리스트의 이름을 변경할 수 없습니다.</string>
|
||||
<string name="error_delete_list">리스트를 삭제할 수 없습니다.</string>
|
||||
|
|
|
@ -259,7 +259,6 @@
|
|||
<string name="add_account_description">Een nieuw Mastodonaccount toevoegen</string>
|
||||
<string name="action_lists">Lijsten</string>
|
||||
<string name="title_lists">Lijsten</string>
|
||||
<string name="title_list_timeline">Tijdlijn lijst</string>
|
||||
<string name="compose_active_account_description">Aan het publiceren met account %1$s</string>
|
||||
<string name="error_failed_set_caption">Toevoegen van beschrijving mislukt</string>
|
||||
<plurals name="hint_describe_for_visually_impaired">
|
||||
|
|
|
@ -242,7 +242,6 @@
|
|||
<string name="add_account_description">Legg til ny Mastodon-konto</string>
|
||||
<string name="action_lists">Lister</string>
|
||||
<string name="title_lists">Lister</string>
|
||||
<string name="title_list_timeline">Listetidslinje</string>
|
||||
<string name="error_create_list">Kunne ikke opprette liste</string>
|
||||
<string name="error_rename_list">Kunne ikke gi liste nytt navn</string>
|
||||
<string name="error_delete_list">Kunne ikke slette liste</string>
|
||||
|
|
|
@ -228,7 +228,6 @@
|
|||
<string name="add_account_description">Apondre un nòu compte Mastodon</string>
|
||||
<string name="action_lists">Listas</string>
|
||||
<string name="title_lists">Listas</string>
|
||||
<string name="title_list_timeline">Flux de la lista</string>
|
||||
<string name="compose_active_account_description">Publicar amb lo compte %1$s</string>
|
||||
<string name="error_failed_set_caption">Fracàs en apondre una legenda</string>
|
||||
<string name="action_set_caption">Apondre una legenda</string>
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
<string name="title_home">Strona główna</string>
|
||||
<string name="title_notifications">Powiadomienia</string>
|
||||
<string name="title_public_local">Lokalne</string>
|
||||
<string name="title_public_federated">Globalne</string>
|
||||
<string name="title_public_federated">Sfederowane</string>
|
||||
<string name="title_view_thread">Wątek</string>
|
||||
<string name="title_posts">Wpisy</string>
|
||||
<string name="title_posts_with_replies">Z odpowiedziami</string>
|
||||
|
@ -33,12 +33,12 @@
|
|||
<string name="title_edit_profile">Edytuj profil</string>
|
||||
<string name="title_drafts">Szkice</string>
|
||||
<string name="title_licenses">Licencje</string>
|
||||
<string name="post_boosted_format">%s podbił</string>
|
||||
<string name="post_sensitive_media_title">Wrażliwe treści</string>
|
||||
<string name="post_media_hidden_title">Ukryto zawartość multimedialną</string>
|
||||
<string name="post_boosted_format">%s podbite</string>
|
||||
<string name="post_sensitive_media_title">Treści wrażliwe</string>
|
||||
<string name="post_media_hidden_title">Ukryto multimedia</string>
|
||||
<string name="post_sensitive_media_directions">Naciśnij, aby wyświetlić</string>
|
||||
<string name="post_content_warning_show_more">Pokaż więcej</string>
|
||||
<string name="post_content_warning_show_less">Ukryj</string>
|
||||
<string name="post_content_warning_show_less">Pokaż mniej</string>
|
||||
<string name="footer_empty">Pusto tutaj. Pociągnij, aby odświeżyć!</string>
|
||||
<string name="notification_reblog_format">%s podbił(-a) Twój wpis</string>
|
||||
<string name="notification_favourite_format">%s dodał Twój post do ulubionych</string>
|
||||
|
@ -95,13 +95,13 @@
|
|||
<string name="action_emoji_keyboard">Klawiatura emoji</string>
|
||||
<string name="download_image">Pobieranie %1$s</string>
|
||||
<string name="action_copy_link">Skopiuj odnośnik</string>
|
||||
<string name="send_post_link_to">Udostępnij odnośnik do wpisu…</string>
|
||||
<string name="send_post_link_to">Udostępnij URL do…</string>
|
||||
<string name="send_post_content_to">Udostępnij wpis do…</string>
|
||||
<string name="confirmation_reported">Wyślij!</string>
|
||||
<string name="confirmation_unblocked">Odblokowano użytkownika</string>
|
||||
<string name="confirmation_unmuted">Cofnięto wyciszenie użytkownika</string>
|
||||
<string name="post_sent">Wyślij!</string>
|
||||
<string name="post_sent_long">Pomyślnie wysłano odpowiedź.</string>
|
||||
<string name="post_sent_long">Odpowiedź wysłano pomyślnie.</string>
|
||||
<string name="hint_domain">Jaka instancja?</string>
|
||||
<string name="hint_compose">Co Ci chodzi po głowie?</string>
|
||||
<string name="hint_content_warning">Ostrzeżenie o zawartości</string>
|
||||
|
@ -151,7 +151,7 @@
|
|||
<string name="pref_title_custom_tabs">Używaj niestandardowych kart Chrome</string>
|
||||
<string name="pref_title_hide_follow_button">Ukryj przycisk śledzenia podczas przewijania</string>
|
||||
<string name="pref_title_post_filter">Filtrowanie osi czasu</string>
|
||||
<string name="pref_title_post_tabs">Zakładki</string>
|
||||
<string name="pref_title_post_tabs">Karty</string>
|
||||
<string name="pref_title_show_boosts">Pokaż podbicia</string>
|
||||
<string name="pref_title_show_replies">Pokazuj odpowiedzi</string>
|
||||
<string name="pref_title_show_media_preview">Pokazuj podgląd zawartości multimedialnej</string>
|
||||
|
@ -183,10 +183,10 @@
|
|||
<string name="notification_summary_medium">%1$s, %2$s, i %3$s</string>
|
||||
<string name="notification_summary_small">%1$s i %2$s</string>
|
||||
<plurals name="notification_title_summary">
|
||||
<item quantity="one">%d nowe powiadomienie</item>
|
||||
<item quantity="few">%d nowe powiadomienia</item>
|
||||
<item quantity="many">%d nowych powiadomień</item>
|
||||
<item quantity="other">%d nowych powiadomień</item>
|
||||
<item quantity="one">%d nowa interakcja</item>
|
||||
<item quantity="few">%d nowe interakcje</item>
|
||||
<item quantity="many">%d nowych interakcji</item>
|
||||
<item quantity="other">%d nowych interakcji</item>
|
||||
</plurals>
|
||||
<string name="description_account_locked">Konto zablokowane</string>
|
||||
<string name="about_title_activity">O programie</string>
|
||||
|
@ -229,7 +229,6 @@
|
|||
<string name="add_account_description">Dodaj nowe Konto Mastodon</string>
|
||||
<string name="action_lists">Listy</string>
|
||||
<string name="title_lists">Listy</string>
|
||||
<string name="title_list_timeline">Oś czasu listy</string>
|
||||
<string name="compose_active_account_description">Publikowanie z konta %1$s</string>
|
||||
<string name="error_failed_set_caption">Nie udało się ustawić podpisu</string>
|
||||
<string name="action_set_caption">Ustaw podpis</string>
|
||||
|
@ -404,25 +403,25 @@
|
|||
<string name="poll_ended_voted">Głosowanie w którym brałeś(-aś) udział zakończyła się</string>
|
||||
<string name="poll_ended_created">Ankieta, którą stworzyłeś(aś), zakończyła się</string>
|
||||
<plurals name="poll_timespan_days">
|
||||
<item quantity="one">Zostało %d dzień</item>
|
||||
<item quantity="one">Został %d dzień</item>
|
||||
<item quantity="few">Zostało %d dni</item>
|
||||
<item quantity="many">Zostało %d dni</item>
|
||||
<item quantity="other">Zostało %d dni</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_hours">
|
||||
<item quantity="one">Zostało %d godzina</item>
|
||||
<item quantity="one">Została %d godzina</item>
|
||||
<item quantity="few">Zostało %d godziny</item>
|
||||
<item quantity="many">Zostało %d godzin</item>
|
||||
<item quantity="other">Zostało %d godzin</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_minutes">
|
||||
<item quantity="one">Zostało %d minuta</item>
|
||||
<item quantity="one">Została %d minuta</item>
|
||||
<item quantity="few">Zostało %d minuty</item>
|
||||
<item quantity="many">Zostało %d minut</item>
|
||||
<item quantity="other">Zostało %d minut</item>
|
||||
</plurals>
|
||||
<plurals name="poll_timespan_seconds">
|
||||
<item quantity="one">Zostało %d sekunda</item>
|
||||
<item quantity="one">Została %d sekunda</item>
|
||||
<item quantity="few">Zostało %d sekund</item>
|
||||
<item quantity="many">Zostało %d sekund</item>
|
||||
<item quantity="other">Zostało %d sekund</item>
|
||||
|
@ -462,7 +461,7 @@
|
|||
<string name="title_bookmarks">Zakładki</string>
|
||||
<string name="action_bookmark">Dodaj do zakładek</string>
|
||||
<string name="action_view_bookmarks">Zakładki</string>
|
||||
<string name="description_post_bookmarked">Dodane do zakładek</string>
|
||||
<string name="description_post_bookmarked">Dodany do zakładek</string>
|
||||
<string name="select_list_title">Wybierz listę</string>
|
||||
<string name="list">Lista</string>
|
||||
<string name="error_audio_upload_size">Pliki audio muszą być mniejsze niż 40MB.</string>
|
||||
|
@ -493,8 +492,8 @@
|
|||
<string name="pref_main_nav_position_option_bottom">Dół</string>
|
||||
<string name="pref_main_nav_position_option_top">Góra</string>
|
||||
<plurals name="error_upload_max_media_reached">
|
||||
<item quantity="one">Nie możesz przesłać więcej niż %1$d załącznika.</item>
|
||||
<item quantity="few">Nie możesz przesłać więcej niż %1$d załączników.</item>
|
||||
<item quantity="one">Nie możesz przesłać więcej niż %1$d załącznik.</item>
|
||||
<item quantity="few">Nie możesz przesłać więcej niż %1$d załączniki.</item>
|
||||
<item quantity="many">Nie możesz przesłać więcej niż %1$d załączników.</item>
|
||||
<item quantity="other">Nie możesz przesłać więcej niż %1$d załączników.</item>
|
||||
</plurals>
|
||||
|
@ -515,21 +514,21 @@
|
|||
<string name="pref_title_enable_swipe_for_tabs">Włącz gest przesuwania by przełączać między zakładkami</string>
|
||||
<string name="post_media_attachments">Załączniki</string>
|
||||
<string name="notification_follow_request_description">Powiadomienia o prośbach o obserwowanie</string>
|
||||
<string name="pref_title_notification_filter_subscriptions">ktoś kogo zasubskrybowałem/zasubskrybowałam opublikował nowy wpis</string>
|
||||
<string name="pref_title_notification_filter_subscriptions">ktoś zasubskrybowany opublikował nowy wpis</string>
|
||||
<string name="pref_title_notification_filter_follow_requests">Wysłano prośbę o obserwowanie</string>
|
||||
<string name="title_announcements">Ogłoszenia</string>
|
||||
<string name="pref_title_wellbeing_mode">Zdrowie</string>
|
||||
<string name="action_unsubscribe_account">Anuluj subskrypcję</string>
|
||||
<string name="action_subscribe_account">Zasubskrybuj</string>
|
||||
<string name="follow_requests_info">Mimo tego, że twoje konto nie jest zablokowane, administracja %1$s uznała, że możesz chcieć ręcznie przejrzeć te prośby o możliwość śledzenia od tych kont.</string>
|
||||
<string name="drafts_post_reply_removed">Wpis dla którego naszkicowałeś/naszkicowałaś odpowiedź został usunięty</string>
|
||||
<string name="drafts_post_reply_removed">Wpis dla którego naszkicowałeś/aś odpowiedź został usunięty</string>
|
||||
<string name="draft_deleted">Usunięto szkic</string>
|
||||
<string name="wellbeing_hide_stats_profile">Ukryj ilościowe statystyki na profilach</string>
|
||||
<string name="wellbeing_hide_stats_posts">Ukryj ilościowe statystyki na postach</string>
|
||||
<string name="review_notifications">Przejrzyj powiadomienia</string>
|
||||
<string name="account_note_saved">Zapisano!</string>
|
||||
<string name="account_note_hint">Twoja prywatna notatka o tym koncie</string>
|
||||
<string name="duration_indefinite">Nieskończona</string>
|
||||
<string name="duration_indefinite">Nieograniczony</string>
|
||||
<string name="post_media_audio">Dźwięk</string>
|
||||
<string name="notification_subscription_description">Powiadomienia o opublikowaniu nowego wpisu przez kogoś, kogo obserwujesz</string>
|
||||
<string name="pref_main_nav_position">Pozycja głównego paska nawigacji</string>
|
||||
|
@ -552,9 +551,11 @@
|
|||
<string name="notification_sign_up_format">%s zarejestrował(a) się</string>
|
||||
<string name="notification_sign_up_name">Rejestracje</string>
|
||||
<string name="notification_sign_up_description">Powiadomienia o nowych użytkownikach</string>
|
||||
<string name="notification_update_description">Powiadomienia o edycji wpisów z którymi interaktowałeś/aś</string>
|
||||
<string name="notification_update_description">Powiadomienia o edycji wpisów z którymi dokonałeś/aś interakcji</string>
|
||||
<string name="pref_title_notification_filter_sign_ups">ktoś zarejestrował się</string>
|
||||
<string name="pref_title_notification_filter_updates">wpis, z którym interaktowałem/am został edytowany</string>
|
||||
<string name="pref_title_notification_filter_updates">wpis, z którym dokonałem/am interakcji został edytowany</string>
|
||||
<string name="notification_update_format">%s edytował(a) swój wpis</string>
|
||||
<string name="notification_update_name">Edycje wpisów</string>
|
||||
<string name="saving_draft">Zapisywanie szkicu…</string>
|
||||
<string name="error_could_not_load_login_page">Nie można załadować strony logowania.</string>
|
||||
</resources>
|
|
@ -246,7 +246,6 @@
|
|||
<string name="add_account_description">Adicionar nova conta Mastodon</string>
|
||||
<string name="action_lists">Listas</string>
|
||||
<string name="title_lists">Listas</string>
|
||||
<string name="title_list_timeline">Linha da lista</string>
|
||||
<string name="compose_active_account_description">Usando a conta %1$s</string>
|
||||
<string name="error_failed_set_caption">Erro ao incluir descrição</string>
|
||||
<string name="action_set_caption">Descrever</string>
|
||||
|
|
|
@ -325,7 +325,7 @@
|
|||
<string name="action_lists">Listas</string>
|
||||
<string name="error_rename_list">Não foi possível renomear a lista</string>
|
||||
<string name="title_lists">Listas</string>
|
||||
<string name="title_list_timeline">Cronologia da timeline</string>
|
||||
|
||||
<string name="error_create_list">Não foi possível criar a lista</string>
|
||||
<string name="error_delete_list">Não foi possível apagar a lista</string>
|
||||
<string name="action_create_list">Criar uma lista</string>
|
||||
|
|
|
@ -296,7 +296,6 @@
|
|||
<string name="add_account_description">Добавить новый акканут Mastodon</string>
|
||||
<string name="action_lists">Списки</string>
|
||||
<string name="title_lists">Списки</string>
|
||||
<string name="title_list_timeline">Список лент</string>
|
||||
<string name="error_create_list">Не удалось создать список</string>
|
||||
<string name="error_rename_list">Не удалось переименовать список</string>
|
||||
<string name="error_delete_list">Не удалось удалить список</string>
|
||||
|
|
|
@ -202,7 +202,6 @@
|
|||
<string name="error_rename_list">पुनः सूचिनामकरणं कर्तुमशक्यम्</string>
|
||||
<string name="error_create_list">सूचिनिर्माणं कर्तुमशक्यम्</string>
|
||||
<string name="dialog_message_cancel_follow_request">अनुसरणानुरोधो नश्यताम् \?</string>
|
||||
<string name="title_list_timeline">सूचेः समयतालिका</string>
|
||||
<string name="title_lists">सूचयः</string>
|
||||
<string name="action_lists">सूचयः</string>
|
||||
<string name="add_account_description">नवमास्टोडोनलेखा युज्यताम्</string>
|
||||
|
|
|
@ -247,7 +247,6 @@
|
|||
<string name="add_account_description">Dodaj nov Mastodon račun</string>
|
||||
<string name="action_lists">Seznami</string>
|
||||
<string name="title_lists">Seznami</string>
|
||||
<string name="title_list_timeline">Seznam časovnice</string>
|
||||
<string name="error_create_list">Seznama ni bilo mogoče ustvariti</string>
|
||||
<string name="error_rename_list">Seznama ni bilo mogoče preimenovati</string>
|
||||
<string name="error_delete_list">Seznama ni bilo mogoče izbrisati</string>
|
||||
|
|
|
@ -269,7 +269,6 @@
|
|||
<string name="add_account_description">Lägg till ett nytt Mastodon-konto</string>
|
||||
<string name="action_lists">Listor</string>
|
||||
<string name="title_lists">Listor</string>
|
||||
<string name="title_list_timeline">Lista tidslinje</string>
|
||||
<string name="error_create_list">Kunde inte skapa lista</string>
|
||||
<string name="error_rename_list">Kunde inte byta namn på lista</string>
|
||||
<string name="error_delete_list">Kunde inte radera lista</string>
|
||||
|
|
|
@ -216,7 +216,6 @@
|
|||
<string name="add_account_description">புதிய Mastodon கணக்கைச் சேர்க்க</string>
|
||||
<string name="action_lists">பட்டியல்கள்</string>
|
||||
<string name="title_lists">பட்டியல்கள்</string>
|
||||
<string name="title_list_timeline">காலவரிசை பட்டியல்</string>
|
||||
<string name="compose_active_account_description">%1$s கணக்குடன் பதிவிட</string>
|
||||
<string name="error_failed_set_caption">தலைப்பை அமைக்க முடியவில்லை</string>
|
||||
<string name="action_set_caption">தலைப்பை அமை</string>
|
||||
|
|
|
@ -144,7 +144,6 @@
|
|||
<string name="error_delete_list">ไม่สามารถลบรายการได้</string>
|
||||
<string name="error_rename_list">ไม่สามารถเปลี่ยนชื่อรายการได้</string>
|
||||
<string name="error_create_list">ไม่สามารถสร้างรายการได้</string>
|
||||
<string name="title_list_timeline">ไทม์ไลน์ในรายการ</string>
|
||||
<string name="add_account_description">เพิ่มบัญชี Mastodon ใหม่</string>
|
||||
<string name="add_account_name">เพิ่มบัญชี</string>
|
||||
<string name="filter_add_description">วลีที่ต้องการกรอง</string>
|
||||
|
|
|
@ -243,7 +243,6 @@
|
|||
<string name="add_account_description">Yeni Mastodon hesabı ekle</string>
|
||||
<string name="action_lists">Listeler</string>
|
||||
<string name="title_lists">Listeler</string>
|
||||
<string name="title_list_timeline">Zaman çizelgesini listele</string>
|
||||
<string name="compose_active_account_description">%1$s hesabıyla gönderiliyor</string>
|
||||
<plurals name="hint_describe_for_visually_impaired">
|
||||
<item quantity="other">Görsel engelli için tanımla
|
||||
|
|
|
@ -285,7 +285,6 @@
|
|||
<string name="error_delete_list">Не вдалося видалити список</string>
|
||||
<string name="error_rename_list">Не вдалося перейменувати список</string>
|
||||
<string name="error_create_list">Не вдалося створити список</string>
|
||||
<string name="title_list_timeline">Стрічка списку</string>
|
||||
<string name="add_account_description">Додати новий обліковий запис Mastodon</string>
|
||||
<string name="add_account_name">Додати обліковий запис</string>
|
||||
<string name="filter_add_description">Фільтрувати фразу</string>
|
||||
|
@ -550,4 +549,5 @@
|
|||
<string name="notification_update_name">Редакції допису</string>
|
||||
<string name="title_login">Вхід</string>
|
||||
<string name="error_could_not_load_login_page">Не вдалося завантажити сторінку входу.</string>
|
||||
<string name="saving_draft">Збереження чернетки…</string>
|
||||
</resources>
|
|
@ -448,7 +448,6 @@
|
|||
<string name="action_delete_list">Xóa danh sách</string>
|
||||
<string name="action_rename_list">Đổi tên danh sách</string>
|
||||
<string name="action_create_list">Tạo danh sách</string>
|
||||
<string name="title_list_timeline">Danh sách bảng tin</string>
|
||||
<string name="add_account_description">Thêm tài khoản Mastodon</string>
|
||||
<string name="add_account_name">Thêm tài khoản</string>
|
||||
<string name="filter_add_description">Thêm mô tả</string>
|
||||
|
@ -517,4 +516,5 @@
|
|||
<string name="notification_update_description">Thông báo khi tút mà tôi tương tác bị sửa</string>
|
||||
<string name="title_login">Đăng nhập</string>
|
||||
<string name="error_could_not_load_login_page">Không thể tải trang đăng nhập.</string>
|
||||
<string name="saving_draft">Đang lưu nháp…</string>
|
||||
</resources>
|
|
@ -277,7 +277,6 @@
|
|||
<string name="add_account_description">添加新的 Mastodon 帐号</string>
|
||||
<string name="action_lists">列表</string>
|
||||
<string name="title_lists">列表</string>
|
||||
<string name="title_list_timeline">列表时间轴</string>
|
||||
<string name="error_create_list">无法新建列表</string>
|
||||
<string name="error_rename_list">无法重命名列表</string>
|
||||
<string name="error_delete_list">无法删除列表</string>
|
||||
|
@ -536,4 +535,5 @@
|
|||
<string name="notification_update_name">嘟文编辑</string>
|
||||
<string name="notification_update_description">当你进行过互动的嘟文被编辑时发出通知</string>
|
||||
<string name="error_could_not_load_login_page">无法加载登录页。</string>
|
||||
<string name="saving_draft">正在保存草稿…</string>
|
||||
</resources>
|
||||
|
|
|
@ -276,7 +276,6 @@
|
|||
<string name="add_account_description">加入新的 Mastodon 帳號</string>
|
||||
<string name="action_lists">列表</string>
|
||||
<string name="title_lists">列表</string>
|
||||
<string name="title_list_timeline">列表時間軸</string>
|
||||
<string name="error_create_list">無法新建列表</string>
|
||||
<string name="error_rename_list">無法重命名列表</string>
|
||||
<string name="error_delete_list">無法刪除列表</string>
|
||||
|
|
|
@ -270,7 +270,6 @@
|
|||
<string name="add_account_description">加入新的 Mastodon 帳號</string>
|
||||
<string name="action_lists">列表</string>
|
||||
<string name="title_lists">列表</string>
|
||||
<string name="title_list_timeline">列表時間軸</string>
|
||||
<string name="error_create_list">無法新建列表</string>
|
||||
<string name="error_rename_list">無法重命名列表</string>
|
||||
<string name="error_delete_list">無法刪除列表</string>
|
||||
|
|
|
@ -274,7 +274,6 @@
|
|||
<string name="add_account_description">添加新的 Mastodon 帐号</string>
|
||||
<string name="action_lists">列表</string>
|
||||
<string name="title_lists">列表</string>
|
||||
<string name="title_list_timeline">列表时间轴</string>
|
||||
<string name="error_create_list">无法新建列表</string>
|
||||
<string name="error_rename_list">无法重命名列表</string>
|
||||
<string name="error_delete_list">无法删除列表</string>
|
||||
|
|
|
@ -276,7 +276,6 @@
|
|||
<string name="add_account_description">加入新的 Mastodon 帳號</string>
|
||||
<string name="action_lists">列表</string>
|
||||
<string name="title_lists">列表</string>
|
||||
<string name="title_list_timeline">列表時間軸</string>
|
||||
<string name="error_create_list">無法新建列表</string>
|
||||
<string name="error_rename_list">無法重命名列表</string>
|
||||
<string name="error_delete_list">無法刪除列表</string>
|
||||
|
@ -525,4 +524,6 @@
|
|||
<string name="pref_title_alway_open_spoiler">總是顯示被標注為內容警告的嘟文</string>
|
||||
<string name="failed_search">搜尋失敗</string>
|
||||
<string name="title_accounts">帳號</string>
|
||||
<string name="title_login">登入</string>
|
||||
<string name="error_could_not_load_login_page">無法載入登入頁面。</string>
|
||||
</resources>
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
<string name="title_mutes">Muted users</string>
|
||||
<string name="title_blocks">Blocked users</string>
|
||||
<string name="title_domain_mutes">Hidden domains</string>
|
||||
<string name="title_migration_relogin">Re-login for push notifications</string>
|
||||
<string name="title_follow_requests">Follow Requests</string>
|
||||
<string name="title_edit_profile">Edit your profile</string>
|
||||
<string name="title_drafts">Drafts</string>
|
||||
|
@ -152,6 +153,8 @@
|
|||
<string name="action_open_reblogger">Open boost author</string>
|
||||
<string name="action_open_reblogged_by">Show boosts</string>
|
||||
<string name="action_open_faved_by">Show favorites</string>
|
||||
<string name="action_dismiss">Dismiss</string>
|
||||
<string name="action_details">Details</string>
|
||||
<string name="action_quote">Quote</string>
|
||||
<string name="action_authorize">Authorize Now!</string>
|
||||
<string name="action_tab_jump_to_top">Jump to top</string>
|
||||
|
@ -413,7 +416,6 @@
|
|||
|
||||
<string name="action_lists">Lists</string>
|
||||
<string name="title_lists">Lists</string>
|
||||
<string name="title_list_timeline">List timeline</string>
|
||||
<string name="error_create_list">Could not create list</string>
|
||||
<string name="error_rename_list">Could not rename list</string>
|
||||
<string name="error_delete_list">Could not delete list</string>
|
||||
|
@ -672,6 +674,14 @@
|
|||
<string name="action_unsubscribe_account">Unsubscribe</string>
|
||||
|
||||
<string name="tusky_compose_post_quicksetting_label">Compose Post</string>
|
||||
|
||||
<string name="account_date_joined">Joined %1$s</string>
|
||||
|
||||
<string name="saving_draft">Saving draft…</string>
|
||||
|
||||
<string name="tips_push_notification_migration">Re-login all accounts to enable push notification support.</string>
|
||||
<string name="dialog_push_notification_migration">In order to use push notifications via UnifiedPush, Tusky needs permission to subscribe to notifications on your Mastodon server. This requires a re-login to change the OAuth scopes granted to Tusky. Using the re-login option here or in Account Preferences will preserve all of your local drafts and cache.</string>
|
||||
<string name="dialog_push_notification_migration_other_accounts">You have re-logged into your current account to grant push subscription permission to Tusky. However, you still have other accounts that have not been migrated this way. Switch to them and re-login one by one in order to enable UnifiedPush notifications support.</string>
|
||||
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -47,6 +47,8 @@ import org.robolectric.Robolectric
|
|||
import org.robolectric.Shadows.shadowOf
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.fakes.RoboMenuItem
|
||||
import java.util.Date
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
/**
|
||||
* Created by charlag on 3/7/18.
|
||||
|
@ -466,22 +468,23 @@ class ComposeActivityTest {
|
|||
null,
|
||||
listOf("en"),
|
||||
Account(
|
||||
"1",
|
||||
"admin",
|
||||
"admin",
|
||||
"admin",
|
||||
"",
|
||||
"https://example.token",
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
null,
|
||||
false,
|
||||
emptyList(),
|
||||
emptyList()
|
||||
id = "1",
|
||||
localUsername = "admin",
|
||||
username = "admin",
|
||||
displayName = "admin",
|
||||
createdAt = Date(),
|
||||
note = "",
|
||||
url = "https://example.token",
|
||||
avatar = "",
|
||||
header = "",
|
||||
locked = false,
|
||||
statusesCount = 0,
|
||||
followersCount = 0,
|
||||
followingCount = 0,
|
||||
source = null,
|
||||
bot = false,
|
||||
emojis = emptyList(),
|
||||
fields = emptyList(),
|
||||
),
|
||||
maximumLegacyTootCharacters,
|
||||
null,
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
package com.keylesspalace.tusky
|
||||
|
||||
import com.keylesspalace.tusky.util.ComposeTokenizer
|
||||
import com.keylesspalace.tusky.components.compose.ComposeTokenizer
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
|
|
@ -5,8 +5,8 @@ buildscript {
|
|||
gradlePluginPortal()
|
||||
}
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:7.1.2"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.20"
|
||||
classpath "com.android.tools.build:gradle:7.2.0"
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21"
|
||||
classpath "org.jlleitschuh.gradle:ktlint-gradle:10.2.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
Tusky v18.0
|
||||
|
||||
- Підтримка нових типів сповіщень Mastodon 3.5
|
||||
- Кращий вигляд позначки бота і розширений вибір тем
|
||||
- Текст тепер можна вибрати у докладному поданні допису
|
||||
- Виправлено безліч помилок, включно з тою, яка перешкоджала входу на Android 6 і старіших
|
|
@ -0,0 +1,6 @@
|
|||
Tusky v18.0
|
||||
|
||||
- Hỗ trợ những kiểu thông báo mới của Mastodon 3.5
|
||||
- Nhãn của tài khoản nhìn đẹp hơn và thay đổi theo chủ đề
|
||||
- Cho phép chọn và sao chép nội dung tút
|
||||
- Sửa lỗi chặn đăng nhập trên Android 6 trở xuống
|
|
@ -0,0 +1,3 @@
|
|||
Tusky v15.1
|
||||
|
||||
此版本修复了给图片添加标题时会崩溃的问题
|
|
@ -0,0 +1,8 @@
|
|||
Tusky v16.0
|
||||
|
||||
- 时间线加载逻辑完全重写,提升了流畅度、稳定性,更便于维护。
|
||||
- APNG和动画WebP格式的动态自定义表情符号。
|
||||
- 修正大量BUG
|
||||
- 支持Android 11
|
||||
- 新增界面语言支持:苏格兰盖尔语、加利西亚语、乌克兰语
|
||||
- 改进翻译
|
Loading…
Reference in New Issue