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 getGitSha = {
|
||||||
def stdout = new ByteArrayOutputStream()
|
def stdout = new ByteArrayOutputStream()
|
||||||
exec {
|
try {
|
||||||
commandLine 'git', 'rev-parse', '--short', 'HEAD'
|
exec {
|
||||||
standardOutput = stdout
|
commandLine 'git', 'rev-parse', '--short', 'HEAD'
|
||||||
|
standardOutput = stdout
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "unknown"
|
||||||
}
|
}
|
||||||
return stdout.toString().trim()
|
return stdout.toString().trim()
|
||||||
}
|
}
|
||||||
|
@ -101,7 +105,7 @@ ext.roomVersion = '2.4.2'
|
||||||
ext.retrofitVersion = '2.9.0'
|
ext.retrofitVersion = '2.9.0'
|
||||||
ext.okhttpVersion = '4.9.3'
|
ext.okhttpVersion = '4.9.3'
|
||||||
ext.glideVersion = '4.13.1'
|
ext.glideVersion = '4.13.1'
|
||||||
ext.daggerVersion = '2.41'
|
ext.daggerVersion = '2.42'
|
||||||
ext.materialdrawerVersion = '8.4.5'
|
ext.materialdrawerVersion = '8.4.5'
|
||||||
ext.emoji2_version = '1.1.0'
|
ext.emoji2_version = '1.1.0'
|
||||||
ext.filemojicompat_version = '3.2.1'
|
ext.filemojicompat_version = '3.2.1'
|
||||||
|
@ -143,7 +147,7 @@ dependencies {
|
||||||
kapt "androidx.room:room-compiler:$roomVersion"
|
kapt "androidx.room:room-compiler:$roomVersion"
|
||||||
implementation 'androidx.core:core-splashscreen:1.0.0-beta02'
|
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"
|
implementation "com.google.code.gson:gson:2.9.0"
|
||||||
|
|
||||||
|
@ -190,6 +194,9 @@ dependencies {
|
||||||
implementation "de.c1710:filemojicompat:$filemojicompat_version"
|
implementation "de.c1710:filemojicompat:$filemojicompat_version"
|
||||||
implementation "de.c1710:filemojicompat-defaults:$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 "androidx.test.ext:junit:1.1.3"
|
||||||
testImplementation "org.robolectric:robolectric:4.4"
|
testImplementation "org.robolectric:robolectric:4.4"
|
||||||
testImplementation "org.mockito:mockito-inline:4.4.0"
|
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
|
<application
|
||||||
android:name=".TuskyApplication"
|
android:name=".TuskyApplication"
|
||||||
|
android:appCategory="social"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
@ -147,6 +148,29 @@
|
||||||
android:name=".receiver.SendStatusBroadcastReceiver"
|
android:name=".receiver.SendStatusBroadcastReceiver"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
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
|
<service
|
||||||
android:name=".service.TuskyTileService"
|
android:name=".service.TuskyTileService"
|
||||||
|
|
|
@ -198,9 +198,9 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onListSelected(listId: String) {
|
private fun onListSelected(listId: String, listTitle: String) {
|
||||||
startActivityWithSlideInAnimation(
|
startActivityWithSlideInAnimation(
|
||||||
StatusListActivity.newListIntent(this, listId)
|
StatusListActivity.newListIntent(this, listId, listTitle)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,7 +270,8 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
override fun onClick(v: View) {
|
||||||
if (v == itemView) {
|
if (v == itemView) {
|
||||||
onListSelected(getItem(bindingAdapterPosition).id)
|
val list = getItem(bindingAdapterPosition)
|
||||||
|
onListSelected(list.id, list.title)
|
||||||
} else {
|
} else {
|
||||||
onMore(getItem(bindingAdapterPosition), v)
|
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.drafts.DraftsActivity
|
||||||
import com.keylesspalace.tusky.components.login.LoginActivity
|
import com.keylesspalace.tusky.components.login.LoginActivity
|
||||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
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.preference.PreferencesActivity
|
||||||
import com.keylesspalace.tusky.components.scheduled.ScheduledStatusActivity
|
import com.keylesspalace.tusky.components.scheduled.ScheduledStatusActivity
|
||||||
import com.keylesspalace.tusky.components.search.SearchActivity
|
import com.keylesspalace.tusky.components.search.SearchActivity
|
||||||
|
@ -267,23 +271,10 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
|
|
||||||
setupTabs(showNotificationTab)
|
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)) {
|
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean(PrefKeys.VIEW_PAGER_OFF_SCREEN_LIMIT, false)) {
|
||||||
binding.viewPager.offscreenPageLimit = 9
|
binding.viewPager.offscreenPageLimit = 9
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup push notifications
|
|
||||||
if (NotificationHelper.areNotificationsEnabled(this, accountManager)) {
|
|
||||||
NotificationHelper.enablePullNotifications(this)
|
|
||||||
} else {
|
|
||||||
NotificationHelper.disablePullNotifications(this)
|
|
||||||
}
|
|
||||||
eventHub.events
|
eventHub.events
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||||
|
@ -796,6 +787,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
(adapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect()
|
(adapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateProfiles()
|
||||||
|
|
||||||
keepScreenOn()
|
keepScreenOn()
|
||||||
return popups
|
return popups
|
||||||
}
|
}
|
||||||
|
@ -811,7 +804,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
}
|
}
|
||||||
// open LoginActivity to add new account
|
// open LoginActivity to add new account
|
||||||
if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) {
|
if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) {
|
||||||
startActivityWithSlideInAnimation(LoginActivity.getIntent(this, true))
|
startActivityWithSlideInAnimation(LoginActivity.getIntent(this, LoginActivity.MODE_ADDITIONAL_LOGIN))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// change Account
|
// change Account
|
||||||
|
@ -842,6 +835,10 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
.setMessage(getString(R.string.action_logout_confirm, activeAccount.fullName))
|
.setMessage(getString(R.string.action_logout_confirm, activeAccount.fullName))
|
||||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||||
lifecycleScope.launch {
|
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)
|
NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, this@MainActivity)
|
||||||
cacheUpdater.clearForUser(activeAccount.id)
|
cacheUpdater.clearForUser(activeAccount.id)
|
||||||
conversationRepository.deleteCacheForAccount(activeAccount.id)
|
conversationRepository.deleteCacheForAccount(activeAccount.id)
|
||||||
|
@ -856,7 +853,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
NotificationHelper.disablePullNotifications(this@MainActivity)
|
NotificationHelper.disablePullNotifications(this@MainActivity)
|
||||||
}
|
}
|
||||||
val intent = if (newAccount == null) {
|
val intent = if (newAccount == null) {
|
||||||
LoginActivity.getIntent(this@MainActivity, false)
|
LoginActivity.getIntent(this@MainActivity, LoginActivity.MODE_DEFAULT)
|
||||||
} else {
|
} else {
|
||||||
Intent(this@MainActivity, MainActivity::class.java)
|
Intent(this@MainActivity, MainActivity::class.java)
|
||||||
}
|
}
|
||||||
|
@ -890,6 +887,16 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
accountManager.updateActiveAccount(me)
|
accountManager.updateActiveAccount(me)
|
||||||
NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this)
|
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
|
accountLocked = me.locked
|
||||||
|
|
||||||
updateProfiles()
|
updateProfiles()
|
||||||
|
|
|
@ -46,7 +46,7 @@ class SplashActivity : AppCompatActivity(), Injectable {
|
||||||
val intent = if (accountManager.activeAccount != null) {
|
val intent = if (accountManager.activeAccount != null) {
|
||||||
Intent(this, MainActivity::class.java)
|
Intent(this, MainActivity::class.java)
|
||||||
} else {
|
} else {
|
||||||
LoginActivity.getIntent(this, false)
|
LoginActivity.getIntent(this, LoginActivity.MODE_DEFAULT)
|
||||||
}
|
}
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
finish()
|
finish()
|
||||||
|
|
|
@ -59,7 +59,7 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
Kind.FAVOURITES -> getString(R.string.title_favourites)
|
Kind.FAVOURITES -> getString(R.string.title_favourites)
|
||||||
Kind.BOOKMARKS -> getString(R.string.title_bookmarks)
|
Kind.BOOKMARKS -> getString(R.string.title_bookmarks)
|
||||||
Kind.TAG -> getString(R.string.title_tag).format(hashtag)
|
Kind.TAG -> getString(R.string.title_tag).format(hashtag)
|
||||||
else -> getString(R.string.title_list_timeline)
|
else -> intent.getStringExtra(EXTRA_LIST_TITLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
supportActionBar?.run {
|
supportActionBar?.run {
|
||||||
|
@ -94,6 +94,7 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
|
|
||||||
private const val EXTRA_KIND = "kind"
|
private const val EXTRA_KIND = "kind"
|
||||||
private const val EXTRA_LIST_ID = "id"
|
private const val EXTRA_LIST_ID = "id"
|
||||||
|
private const val EXTRA_LIST_TITLE = "title"
|
||||||
private const val EXTRA_HASHTAG = "tag"
|
private const val EXTRA_HASHTAG = "tag"
|
||||||
|
|
||||||
fun newFavouritesIntent(context: Context) =
|
fun newFavouritesIntent(context: Context) =
|
||||||
|
@ -106,10 +107,11 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
putExtra(EXTRA_KIND, Kind.BOOKMARKS.name)
|
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 {
|
Intent(context, StatusListActivity::class.java).apply {
|
||||||
putExtra(EXTRA_KIND, Kind.LIST.name)
|
putExtra(EXTRA_KIND, Kind.LIST.name)
|
||||||
putExtra(EXTRA_LIST_ID, listId)
|
putExtra(EXTRA_LIST_ID, listId)
|
||||||
|
putExtra(EXTRA_LIST_TITLE, listTitle)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
|
|
@ -539,8 +539,13 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
message.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
|
message.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
|
||||||
String wholeMessage = String.format(format, displayName);
|
String wholeMessage = String.format(format, displayName);
|
||||||
final SpannableStringBuilder str = new SpannableStringBuilder(wholeMessage);
|
final SpannableStringBuilder str = new SpannableStringBuilder(wholeMessage);
|
||||||
str.setSpan(new StyleSpan(Typeface.BOLD), 0, displayName.length(),
|
int displayNameIndex = format.indexOf("%s");
|
||||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
str.setSpan(
|
||||||
|
new StyleSpan(Typeface.BOLD),
|
||||||
|
displayNameIndex,
|
||||||
|
displayNameIndex + displayName.length(),
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
);
|
||||||
CharSequence emojifiedText = CustomEmojiHelper.emojify(
|
CharSequence emojifiedText = CustomEmojiHelper.emojify(
|
||||||
str, notificationViewData.getAccount().getEmojis(), message, statusDisplayOptions.animateEmojis()
|
str, notificationViewData.getAccount().getEmojis(), message, statusDisplayOptions.animateEmojis()
|
||||||
);
|
);
|
||||||
|
|
|
@ -84,6 +84,9 @@ import com.keylesspalace.tusky.view.showMuteAccountDialog
|
||||||
import dagger.android.DispatchingAndroidInjector
|
import dagger.android.DispatchingAndroidInjector
|
||||||
import dagger.android.HasAndroidInjector
|
import dagger.android.HasAndroidInjector
|
||||||
import java.text.NumberFormat
|
import java.text.NumberFormat
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
@ -418,6 +421,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
updateToolbar()
|
updateToolbar()
|
||||||
updateMovedAccount()
|
updateMovedAccount()
|
||||||
updateRemoteAccount()
|
updateRemoteAccount()
|
||||||
|
updateAccountJoinedDate()
|
||||||
updateAccountStats()
|
updateAccountStats()
|
||||||
invalidateOptionsMenu()
|
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
|
* 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.NewPoll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
import com.keylesspalace.tusky.util.ComposeTokenizer
|
|
||||||
import com.keylesspalace.tusky.util.PickMediaFiles
|
import com.keylesspalace.tusky.util.PickMediaFiles
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils
|
import com.keylesspalace.tusky.util.ThemeUtils
|
||||||
import com.keylesspalace.tusky.util.afterTextChanged
|
import com.keylesspalace.tusky.util.afterTextChanged
|
||||||
|
@ -361,7 +360,8 @@ class ComposeActivity :
|
||||||
ComposeAutoCompleteAdapter(
|
ComposeAutoCompleteAdapter(
|
||||||
this,
|
this,
|
||||||
preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
|
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())
|
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,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
package com.keylesspalace.tusky.util
|
package com.keylesspalace.tusky.components.compose
|
||||||
|
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
|
@ -24,6 +24,7 @@ import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.asLiveData
|
import androidx.lifecycle.asLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
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.drafts.DraftHelper
|
||||||
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfo
|
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfo
|
||||||
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
|
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.service.StatusToSend
|
||||||
import com.keylesspalace.tusky.util.combineLiveData
|
import com.keylesspalace.tusky.util.combineLiveData
|
||||||
import com.keylesspalace.tusky.util.randomAlphanumericString
|
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||||
|
import com.keylesspalace.tusky.util.result
|
||||||
import com.keylesspalace.tusky.util.toLiveData
|
import com.keylesspalace.tusky.util.toLiveData
|
||||||
import io.reactivex.rxjava3.core.Observable
|
import io.reactivex.rxjava3.core.Observable
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -51,7 +53,6 @@ import kotlinx.coroutines.flow.updateAndGet
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.rx3.rxSingle
|
import kotlinx.coroutines.rx3.rxSingle
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.Locale
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ComposeViewModel @Inject constructor(
|
class ComposeViewModel @Inject constructor(
|
||||||
|
@ -195,7 +196,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
|
|
||||||
fun removeMediaFromQueue(item: QueuedMedia) {
|
fun removeMediaFromQueue(item: QueuedMedia) {
|
||||||
mediaToJob[item.localId]?.cancel()
|
mediaToJob[item.localId]?.cancel()
|
||||||
media.update { mediaValue -> mediaValue.filter { it.localId == item.localId } }
|
media.update { mediaValue -> mediaValue.filter { it.localId != item.localId } }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleMarkSensitive() {
|
fun toggleMarkSensitive() {
|
||||||
|
@ -342,48 +343,39 @@ class ComposeViewModel @Inject constructor(
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun searchAutocompleteSuggestions(token: String): List<ComposeAutoCompleteAdapter.AutocompleteResult> {
|
fun searchAutocompleteSuggestions(token: String): List<AutocompleteResult> {
|
||||||
when (token[0]) {
|
when (token[0]) {
|
||||||
'@' -> {
|
'@' -> {
|
||||||
return try {
|
return api.searchAccountsCall(query = token.substring(1), limit = 10)
|
||||||
api.searchAccounts(query = token.substring(1), limit = 10)
|
.result()
|
||||||
.blockingGet()
|
.fold({ accounts ->
|
||||||
.map { ComposeAutoCompleteAdapter.AccountResult(it) }
|
accounts.map { AutocompleteResult.AccountResult(it) }
|
||||||
} catch (e: Throwable) {
|
}, { e ->
|
||||||
Log.e(TAG, String.format("Autocomplete search for %s failed.", token), e)
|
Log.e(TAG, "Autocomplete search for $token failed.", e)
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
'#' -> {
|
'#' -> {
|
||||||
return try {
|
return api.searchCall(query = token, type = SearchType.Hashtag.apiParameter, limit = 10)
|
||||||
api.searchObservable(query = token, type = SearchType.Hashtag.apiParameter, limit = 10)
|
.result()
|
||||||
.blockingGet()
|
.fold({ searchResult ->
|
||||||
.hashtags
|
searchResult.hashtags.map { AutocompleteResult.HashtagResult(it.name) }
|
||||||
.map { ComposeAutoCompleteAdapter.HashtagResult(it) }
|
}, { e ->
|
||||||
} catch (e: Throwable) {
|
Log.e(TAG, "Autocomplete search for $token failed.", e)
|
||||||
Log.e(TAG, String.format("Autocomplete search for %s failed.", token), e)
|
emptyList()
|
||||||
emptyList()
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
':' -> {
|
':' -> {
|
||||||
val emojiList = emoji.value ?: return emptyList()
|
val emojiList = emoji.value ?: return emptyList()
|
||||||
|
val incomplete = token.substring(1)
|
||||||
|
|
||||||
val incomplete = token.substring(1).lowercase(Locale.ROOT)
|
return emojiList.filter { emoji ->
|
||||||
val results = ArrayList<ComposeAutoCompleteAdapter.AutocompleteResult>()
|
emoji.shortcode.contains(incomplete, ignoreCase = true)
|
||||||
val resultsInside = ArrayList<ComposeAutoCompleteAdapter.AutocompleteResult>()
|
}.sortedBy { emoji ->
|
||||||
for (emoji in emojiList) {
|
emoji.shortcode.indexOf(incomplete, ignoreCase = true)
|
||||||
val shortcode = emoji.shortcode.lowercase(Locale.ROOT)
|
}.map { emoji ->
|
||||||
if (shortcode.startsWith(incomplete)) {
|
AutocompleteResult.EmojiResult(emoji)
|
||||||
results.add(ComposeAutoCompleteAdapter.EmojiResult(emoji))
|
|
||||||
} else if (shortcode.indexOf(incomplete, 1) != -1) {
|
|
||||||
resultsInside.add(ComposeAutoCompleteAdapter.EmojiResult(emoji))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (results.isNotEmpty() && resultsInside.isNotEmpty()) {
|
|
||||||
results.add(ComposeAutoCompleteAdapter.ResultSeparator())
|
|
||||||
}
|
|
||||||
results.addAll(resultsInside)
|
|
||||||
return results
|
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Log.w(TAG, "Unexpected autocompletion token: $token")
|
Log.w(TAG, "Unexpected autocompletion token: $token")
|
||||||
|
|
|
@ -90,12 +90,17 @@ class LoginActivity : BaseActivity(), Injectable {
|
||||||
|
|
||||||
if (savedInstanceState == null &&
|
if (savedInstanceState == null &&
|
||||||
BuildConfig.CUSTOM_INSTANCE.isNotBlank() &&
|
BuildConfig.CUSTOM_INSTANCE.isNotBlank() &&
|
||||||
!isAdditionalLogin()
|
!isAdditionalLogin() && !isAccountMigration()
|
||||||
) {
|
) {
|
||||||
binding.domainEditText.setText(BuildConfig.CUSTOM_INSTANCE)
|
binding.domainEditText.setText(BuildConfig.CUSTOM_INSTANCE)
|
||||||
binding.domainEditText.setSelection(BuildConfig.CUSTOM_INSTANCE.length)
|
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()) {
|
if (BuildConfig.CUSTOM_LOGO_URL.isNotBlank()) {
|
||||||
Glide.with(binding.loginLogo)
|
Glide.with(binding.loginLogo)
|
||||||
.load(BuildConfig.CUSTOM_LOGO_URL)
|
.load(BuildConfig.CUSTOM_LOGO_URL)
|
||||||
|
@ -118,7 +123,7 @@ class LoginActivity : BaseActivity(), Injectable {
|
||||||
textView?.movementMethod = LinkMovementMethod.getInstance()
|
textView?.movementMethod = LinkMovementMethod.getInstance()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAdditionalLogin()) {
|
if (isAdditionalLogin() || isAccountMigration()) {
|
||||||
setSupportActionBar(binding.toolbar)
|
setSupportActionBar(binding.toolbar)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
supportActionBar?.setDisplayShowTitleEnabled(false)
|
supportActionBar?.setDisplayShowTitleEnabled(false)
|
||||||
|
@ -133,7 +138,7 @@ class LoginActivity : BaseActivity(), Injectable {
|
||||||
|
|
||||||
override fun finish() {
|
override fun finish() {
|
||||||
super.finish()
|
super.finish()
|
||||||
if (isAdditionalLogin()) {
|
if (isAdditionalLogin() || isAccountMigration()) {
|
||||||
overridePendingTransition(R.anim.slide_from_left, R.anim.slide_to_right)
|
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"
|
domain, clientId, clientSecret, oauthRedirectUri, code, "authorization_code"
|
||||||
).fold(
|
).fold(
|
||||||
{ accessToken ->
|
{ accessToken ->
|
||||||
accountManager.addAccount(accessToken.accessToken, domain)
|
accountManager.addAccount(accessToken.accessToken, domain, OAUTH_SCOPES)
|
||||||
|
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
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 {
|
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 {
|
companion object {
|
||||||
private const val TAG = "LoginActivity" // logging tag
|
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 LOGIN_MODE = "LOGIN_MODE"
|
||||||
private const val DOMAIN = "domain"
|
private const val DOMAIN = "domain"
|
||||||
private const val CLIENT_ID = "clientId"
|
private const val CLIENT_ID = "clientId"
|
||||||
private const val CLIENT_SECRET = "clientSecret"
|
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
|
@JvmStatic
|
||||||
fun getIntent(context: Context, mode: Boolean): Intent {
|
fun getIntent(context: Context, mode: Int): Intent {
|
||||||
val loginIntent = Intent(context, LoginActivity::class.java)
|
val loginIntent = Intent(context, LoginActivity::class.java)
|
||||||
loginIntent.putExtra(LOGIN_MODE, mode)
|
loginIntent.putExtra(LOGIN_MODE, mode)
|
||||||
return loginIntent
|
return loginIntent
|
||||||
|
|
|
@ -57,6 +57,7 @@ import com.keylesspalace.tusky.entity.Notification;
|
||||||
import com.keylesspalace.tusky.entity.Poll;
|
import com.keylesspalace.tusky.entity.Poll;
|
||||||
import com.keylesspalace.tusky.entity.PollOption;
|
import com.keylesspalace.tusky.entity.PollOption;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi;
|
||||||
import com.keylesspalace.tusky.receiver.NotificationClearBroadcastReceiver;
|
import com.keylesspalace.tusky.receiver.NotificationClearBroadcastReceiver;
|
||||||
import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver;
|
import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver;
|
||||||
import com.keylesspalace.tusky.util.StringUtils;
|
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) {
|
Context context) {
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
|
||||||
String channelId = getChannelId(account, notification);
|
String channelId = getChannelId(account, type);
|
||||||
if(channelId == null) {
|
if(channelId == null) {
|
||||||
// unknown notificationtype
|
// unknown notificationtype
|
||||||
return false;
|
return false;
|
||||||
|
@ -554,7 +560,7 @@ public class NotificationHelper {
|
||||||
return channel.getImportance() > NotificationManager.IMPORTANCE_NONE;
|
return channel.getImportance() > NotificationManager.IMPORTANCE_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (notification.getType()) {
|
switch (type) {
|
||||||
case MENTION:
|
case MENTION:
|
||||||
return account.getNotificationsMentioned();
|
return account.getNotificationsMentioned();
|
||||||
case STATUS:
|
case STATUS:
|
||||||
|
@ -580,7 +586,12 @@ public class NotificationHelper {
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private static String getChannelId(AccountEntity account, Notification notification) {
|
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:
|
case MENTION:
|
||||||
return CHANNEL_MENTION + account.getIdentifier();
|
return CHANNEL_MENTION + account.getIdentifier();
|
||||||
case STATUS:
|
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 androidx.preference.PreferenceFragmentCompat
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.keylesspalace.tusky.AccountListActivity
|
import com.keylesspalace.tusky.AccountListActivity
|
||||||
|
import com.keylesspalace.tusky.BaseActivity
|
||||||
import com.keylesspalace.tusky.BuildConfig
|
import com.keylesspalace.tusky.BuildConfig
|
||||||
import com.keylesspalace.tusky.FiltersActivity
|
import com.keylesspalace.tusky.FiltersActivity
|
||||||
import com.keylesspalace.tusky.R
|
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.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||||
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
||||||
|
import com.keylesspalace.tusky.components.login.LoginActivity
|
||||||
|
import com.keylesspalace.tusky.components.notifications.currentAccountNeedsMigration
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
import com.keylesspalace.tusky.db.AccountEntity
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
|
@ -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) {
|
preferenceCategory(R.string.pref_publishing) {
|
||||||
listPreference {
|
listPreference {
|
||||||
setTitle(R.string.pref_default_post_privacy)
|
setTitle(R.string.pref_default_post_privacy)
|
||||||
|
|
|
@ -64,7 +64,15 @@ data class AccountEntity(
|
||||||
var activeNotifications: String = "[]",
|
var activeNotifications: String = "[]",
|
||||||
var emojis: List<Emoji> = emptyList(),
|
var emojis: List<Emoji> = emptyList(),
|
||||||
var tabPreferences: List<TabData> = defaultTabs(),
|
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
|
val identifier: String
|
||||||
|
|
|
@ -54,7 +54,7 @@ class AccountManager @Inject constructor(db: AppDatabase) {
|
||||||
* @param accessToken the access token for the new account
|
* @param accessToken the access token for the new account
|
||||||
* @param domain the domain of the accounts Mastodon instance
|
* @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 {
|
activeAccount?.let {
|
||||||
it.isActive = false
|
it.isActive = false
|
||||||
|
@ -65,7 +65,10 @@ class AccountManager @Inject constructor(db: AppDatabase) {
|
||||||
|
|
||||||
val maxAccountId = accounts.maxByOrNull { it.id }?.id ?: 0
|
val maxAccountId = accounts.maxByOrNull { it.id }?.id ?: 0
|
||||||
val newAccountId = maxAccountId + 1
|
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
|
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,
|
@Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
||||||
TimelineAccountEntity.class, ConversationEntity.class
|
TimelineAccountEntity.class, ConversationEntity.class
|
||||||
}, version = 35)
|
}, version = 36)
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
|
|
||||||
public abstract AccountDao accountDao();
|
public abstract AccountDao accountDao();
|
||||||
|
@ -542,4 +542,16 @@ public abstract class AppDatabase extends RoomDatabase {
|
||||||
database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `card` TEXT");
|
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_26_27, AppDatabase.MIGRATION_27_28, AppDatabase.MIGRATION_28_29,
|
||||||
AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31, AppDatabase.MIGRATION_31_32,
|
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_32_33, AppDatabase.MIGRATION_33_34, AppDatabase.MIGRATION_34_35,
|
||||||
|
AppDatabase.MIGRATION_35_36,
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,8 +16,10 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.di
|
package com.keylesspalace.tusky.di
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.receiver.NotificationBlockStateBroadcastReceiver
|
||||||
import com.keylesspalace.tusky.receiver.NotificationClearBroadcastReceiver
|
import com.keylesspalace.tusky.receiver.NotificationClearBroadcastReceiver
|
||||||
import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver
|
import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver
|
||||||
|
import com.keylesspalace.tusky.receiver.UnifiedPushBroadcastReceiver
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.android.ContributesAndroidInjector
|
import dagger.android.ContributesAndroidInjector
|
||||||
|
|
||||||
|
@ -28,4 +30,10 @@ abstract class BroadcastReceiverModule {
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributeNotificationClearBroadcastReceiver(): NotificationClearBroadcastReceiver
|
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("username") val localUsername: String,
|
||||||
@SerializedName("acct", alternate = ["subject"]) val username: 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("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 note: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
val avatar: 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.MediaUploadResult
|
||||||
import com.keylesspalace.tusky.entity.NewStatus
|
import com.keylesspalace.tusky.entity.NewStatus
|
||||||
import com.keylesspalace.tusky.entity.Notification
|
import com.keylesspalace.tusky.entity.Notification
|
||||||
|
import com.keylesspalace.tusky.entity.NotificationSubscribeResult
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Relationship
|
import com.keylesspalace.tusky.entity.Relationship
|
||||||
import com.keylesspalace.tusky.entity.ScheduledStatus
|
import com.keylesspalace.tusky.entity.ScheduledStatus
|
||||||
|
@ -47,6 +48,7 @@ import retrofit2.Response
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
import retrofit2.http.DELETE
|
import retrofit2.http.DELETE
|
||||||
import retrofit2.http.Field
|
import retrofit2.http.Field
|
||||||
|
import retrofit2.http.FieldMap
|
||||||
import retrofit2.http.FormUrlEncoded
|
import retrofit2.http.FormUrlEncoded
|
||||||
import retrofit2.http.GET
|
import retrofit2.http.GET
|
||||||
import retrofit2.http.HTTP
|
import retrofit2.http.HTTP
|
||||||
|
@ -286,6 +288,14 @@ interface MastodonApi {
|
||||||
@Query("following") following: Boolean? = null
|
@Query("following") following: Boolean? = null
|
||||||
): Single<List<TimelineAccount>>
|
): 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}")
|
@GET("api/v1/accounts/{id}")
|
||||||
fun account(
|
fun account(
|
||||||
@Path("id") accountId: String
|
@Path("id") accountId: String
|
||||||
|
@ -591,10 +601,48 @@ interface MastodonApi {
|
||||||
@Query("following") following: Boolean? = null
|
@Query("following") following: Boolean? = null
|
||||||
): Single<SearchResult>
|
): 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
|
@FormUrlEncoded
|
||||||
@POST("api/v1/accounts/{id}/note")
|
@POST("api/v1/accounts/{id}/note")
|
||||||
fun updateAccountNote(
|
fun updateAccountNote(
|
||||||
@Path("id") accountId: String,
|
@Path("id") accountId: String,
|
||||||
@Field("comment") note: String
|
@Field("comment") note: String
|
||||||
): Single<Relationship>
|
): 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) {
|
private void authSucceeded(String domain, String accessToken) {
|
||||||
accountManager.addAccount(accessToken, domain);
|
accountManager.addAccount(accessToken, domain, "");
|
||||||
log("Completed. Enjoy!");
|
log("Completed. Enjoy!");
|
||||||
|
|
||||||
Intent intent = new Intent(this, MainActivity.class);
|
Intent intent = new Intent(this, MainActivity.class);
|
||||||
|
|
|
@ -235,6 +235,19 @@
|
||||||
tools:itemCount="2"
|
tools:itemCount="2"
|
||||||
tools:listitem="@layout/item_account_field" />
|
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
|
<TextView
|
||||||
android:id="@+id/accountRemoveView"
|
android:id="@+id/accountRemoveView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -245,7 +258,7 @@
|
||||||
android:lineSpacingMultiplier="1.1"
|
android:lineSpacingMultiplier="1.1"
|
||||||
android:text="@string/label_remote_account"
|
android:text="@string/label_remote_account"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintTop_toBottomOf="@id/accountFieldList"
|
app:layout_constraintTop_toBottomOf="@id/accountDateJoined"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
|
|
@ -16,8 +16,8 @@
|
||||||
android:id="@+id/toolbar"
|
android:id="@+id/toolbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="?attr/actionBarSize"
|
android:layout_height="?attr/actionBarSize"
|
||||||
app:contentInsetStartWithNavigation="0dp"
|
|
||||||
android:elevation="@dimen/actionbar_elevation"
|
android:elevation="@dimen/actionbar_elevation"
|
||||||
|
app:contentInsetStartWithNavigation="0dp"
|
||||||
app:layout_scrollFlags="scroll|snap|enterAlways"
|
app:layout_scrollFlags="scroll|snap|enterAlways"
|
||||||
app:navigationIcon="?attr/homeAsUpIndicator" />
|
app:navigationIcon="?attr/homeAsUpIndicator" />
|
||||||
|
|
||||||
|
@ -27,8 +27,9 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:tabGravity="fill"
|
app:tabGravity="fill"
|
||||||
|
app:tabMaxWidth="0dp"
|
||||||
app:tabMode="fixed"
|
app:tabMode="fixed"
|
||||||
app:tabTextAppearance="@style/TuskyTabAppearance"/>
|
app:tabTextAppearance="@style/TuskyTabAppearance" />
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
@ -38,6 +39,6 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
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>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -1,48 +1,65 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center_vertical"
|
android:paddingStart="16dp"
|
||||||
android:padding="8dp">
|
android:paddingTop="8dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:paddingBottom="8dp">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/avatar"
|
android:id="@+id/avatar"
|
||||||
android:layout_width="42dp"
|
android:layout_width="48dp"
|
||||||
android:layout_height="42dp"
|
android:layout_height="48dp"
|
||||||
android:layout_centerVertical="true"
|
android:layout_marginEnd="24dp"
|
||||||
android:layout_marginEnd="8dp"
|
android:foregroundGravity="center_vertical"
|
||||||
android:contentDescription="@null"
|
android:importantForAccessibility="no"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:src="@drawable/avatar_default" />
|
tools:src="@drawable/avatar_default" />
|
||||||
|
|
||||||
<LinearLayout
|
<ImageView
|
||||||
android:layout_width="wrap_content"
|
android:id="@+id/avatarBadge"
|
||||||
android:layout_height="match_parent"
|
android:layout_width="24dp"
|
||||||
android:layout_toEndOf="@id/avatar"
|
android:layout_height="24dp"
|
||||||
android:gravity="center_vertical"
|
android:contentDescription="@string/profile_badge_bot_text"
|
||||||
android:orientation="vertical">
|
android:src="@drawable/bot_badge"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/avatar"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/avatar" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/display_name"
|
android:id="@+id/displayName"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:ellipsize="end"
|
android:layout_marginStart="14dp"
|
||||||
android:maxLines="1"
|
android:ellipsize="end"
|
||||||
android:textColor="?android:textColorPrimary"
|
android:maxLines="1"
|
||||||
android:textSize="?attr/status_text_large"
|
android:textColor="?android:textColorPrimary"
|
||||||
android:textStyle="normal|bold"
|
android:textSize="?attr/status_text_large"
|
||||||
tools:text="Conny Duck" />
|
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
|
<TextView
|
||||||
android:id="@+id/username"
|
android:id="@+id/username"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:ellipsize="end"
|
android:layout_marginStart="14dp"
|
||||||
android:maxLines="1"
|
android:ellipsize="end"
|
||||||
android:textColor="?android:textColorSecondary"
|
android:maxLines="1"
|
||||||
android:textSize="?attr/status_text_medium"
|
android:textColor="?android:textColorSecondary"
|
||||||
tools:text="\@ConnyDuck" />
|
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>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
|
@ -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:layout_height="wrap_content"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:padding="8dp">
|
tools:ignore="UseCompoundDrawables">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/preview"
|
android:id="@+id/preview"
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
android:layout_marginStart="4dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="8dp"
|
||||||
android:layout_marginEnd="4dp"
|
android:layout_marginBottom="8dp"
|
||||||
android:layout_marginBottom="4dp"
|
android:importantForAccessibility="no" />
|
||||||
android:contentDescription="@null"
|
|
||||||
android:padding="4dp" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/shortcode"
|
android:id="@+id/shortcode"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
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:ellipsize="end"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:textColor="?android:textColorPrimary"
|
android:textColor="?android:textColorPrimary"
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
<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:id="@+id/hashtag"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
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:textSize="?attr/status_text_medium"
|
||||||
android:textStyle="normal|bold"
|
android:textStyle="normal|bold"
|
||||||
app:drawableStartCompat="@drawable/ic_list"
|
tools:text="#Tusky" />
|
||||||
app:drawableTint="?attr/iconColor" />
|
|
||||||
|
|
|
@ -269,7 +269,6 @@
|
||||||
<string name="add_account_description">إضافة حساب ماستدون جديد</string>
|
<string name="add_account_description">إضافة حساب ماستدون جديد</string>
|
||||||
<string name="action_lists">القوائم</string>
|
<string name="action_lists">القوائم</string>
|
||||||
<string name="title_lists">القوائم</string>
|
<string name="title_lists">القوائم</string>
|
||||||
<string name="title_list_timeline">الخط الزمني للقائمة</string>
|
|
||||||
<string name="error_create_list">لا يمكن إنشاء قائمة</string>
|
<string name="error_create_list">لا يمكن إنشاء قائمة</string>
|
||||||
<string name="error_rename_list">لا يمكن إعادة تسمية القائمة</string>
|
<string name="error_rename_list">لا يمكن إعادة تسمية القائمة</string>
|
||||||
<string name="error_delete_list">لا يمكن حذف القائمة</string>
|
<string name="error_delete_list">لا يمكن حذف القائمة</string>
|
||||||
|
|
|
@ -151,7 +151,6 @@
|
||||||
<string name="error_delete_list">Списъкът не можа да се изтрие</string>
|
<string name="error_delete_list">Списъкът не можа да се изтрие</string>
|
||||||
<string name="error_create_list">Списъкът не можа да се създаде</string>
|
<string name="error_create_list">Списъкът не можа да се създаде</string>
|
||||||
<string name="error_rename_list">Списъкът не можа да се преименува</string>
|
<string name="error_rename_list">Списъкът не можа да се преименува</string>
|
||||||
<string name="title_list_timeline">Списъчна емисия</string>
|
|
||||||
<string name="title_lists">Списъци</string>
|
<string name="title_lists">Списъци</string>
|
||||||
<string name="action_lists">Списъци</string>
|
<string name="action_lists">Списъци</string>
|
||||||
<string name="add_account_description">Добавяне на нов Mastodon акаунт</string>
|
<string name="add_account_description">Добавяне на нов Mastodon акаунт</string>
|
||||||
|
|
|
@ -75,7 +75,6 @@
|
||||||
<string name="error_delete_list">তালিকা মুছে ফেলা যায়নি</string>
|
<string name="error_delete_list">তালিকা মুছে ফেলা যায়নি</string>
|
||||||
<string name="error_rename_list">তালিকা নামকরণ করা যায়নি</string>
|
<string name="error_rename_list">তালিকা নামকরণ করা যায়নি</string>
|
||||||
<string name="error_create_list">তালিকা তৈরি করা যায়নি</string>
|
<string name="error_create_list">তালিকা তৈরি করা যায়নি</string>
|
||||||
<string name="title_list_timeline">তালিকা টাইমলাইনে রাখুন</string>
|
|
||||||
<string name="title_lists">তালিকাসমূহ</string>
|
<string name="title_lists">তালিকাসমূহ</string>
|
||||||
<string name="action_lists">তালিকাসমূহ</string>
|
<string name="action_lists">তালিকাসমূহ</string>
|
||||||
<string name="add_account_description">নতুন ম্যাস্টোডোন অ্যাকাউন্ট যোগ করুন</string>
|
<string name="add_account_description">নতুন ম্যাস্টোডোন অ্যাকাউন্ট যোগ করুন</string>
|
||||||
|
|
|
@ -275,7 +275,6 @@
|
||||||
<string name="add_account_description">নতুন ম্যাস্টোডোন অ্যাকাউন্ট যোগ করুন</string>
|
<string name="add_account_description">নতুন ম্যাস্টোডোন অ্যাকাউন্ট যোগ করুন</string>
|
||||||
<string name="action_lists">তালিকাসমূহ</string>
|
<string name="action_lists">তালিকাসমূহ</string>
|
||||||
<string name="title_lists">তালিকাসমূহ</string>
|
<string name="title_lists">তালিকাসমূহ</string>
|
||||||
<string name="title_list_timeline">তালিকা টাইমলাইনে রাখুন</string>
|
|
||||||
<string name="error_create_list">তালিকা তৈরি করা যায়নি</string>
|
<string name="error_create_list">তালিকা তৈরি করা যায়নি</string>
|
||||||
<string name="error_rename_list">তালিকা নামকরণ করা যায়নি</string>
|
<string name="error_rename_list">তালিকা নামকরণ করা যায়নি</string>
|
||||||
<string name="error_delete_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="add_account_description">Afegir un compte de Mastodont</string>
|
||||||
<string name="action_lists">Llistes</string>
|
<string name="action_lists">Llistes</string>
|
||||||
<string name="title_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_create_list">És impossible crear la llista</string>
|
||||||
<string name="error_rename_list">Impossible reanomenar la llista</string>
|
<string name="error_rename_list">Impossible reanomenar la llista</string>
|
||||||
<string name="error_delete_list">És impossible suprimir 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_delete_list">نەیتوانی لیستەکە بسڕێتەوە</string>
|
||||||
<string name="error_rename_list">نەیتوانی ناوی لیست بنووسرێ</string>
|
<string name="error_rename_list">نەیتوانی ناوی لیست بنووسرێ</string>
|
||||||
<string name="error_create_list">نەیتوانی لیست دروست بکات</string>
|
<string name="error_create_list">نەیتوانی لیست دروست بکات</string>
|
||||||
<string name="title_list_timeline">لیستی تایم لاین</string>
|
|
||||||
<string name="title_lists">لیستەکان</string>
|
<string name="title_lists">لیستەکان</string>
|
||||||
<string name="action_lists">لیستەکان</string>
|
<string name="action_lists">لیستەکان</string>
|
||||||
<string name="add_account_description">زیادکردنی ئەژمێری ماتۆدۆنی نوێ</string>
|
<string name="add_account_description">زیادکردنی ئەژمێری ماتۆدۆنی نوێ</string>
|
||||||
|
|
|
@ -274,7 +274,6 @@
|
||||||
<string name="add_account_description">Přidat nový účet Mastodon</string>
|
<string name="add_account_description">Přidat nový účet Mastodon</string>
|
||||||
<string name="action_lists">Seznamy</string>
|
<string name="action_lists">Seznamy</string>
|
||||||
<string name="title_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_create_list">Nelze vytvořit seznam</string>
|
||||||
<string name="error_rename_list">Nelze přejmenovat seznam</string>
|
<string name="error_rename_list">Nelze přejmenovat seznam</string>
|
||||||
<string name="error_delete_list">Nelze smazat 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="add_account_description">Ychwanegu cyfrif Mastodon newydd</string>
|
||||||
<string name="action_lists">Rhestri</string>
|
<string name="action_lists">Rhestri</string>
|
||||||
<string name="title_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="compose_active_account_description">Yn postio â chyfrif %1$s</string>
|
||||||
<string name="error_failed_set_caption">Methu gosod pennawd</string>
|
<string name="error_failed_set_caption">Methu gosod pennawd</string>
|
||||||
<string name="action_set_caption">Pennu 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="add_account_description">Neues Mastodon-Konto hinzufügen</string>
|
||||||
<string name="action_lists">Listen</string>
|
<string name="action_lists">Listen</string>
|
||||||
<string name="title_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_create_list">Liste erstellen</string>
|
||||||
<string name="action_rename_list">Liste umbenennen</string>
|
<string name="action_rename_list">Liste umbenennen</string>
|
||||||
<string name="action_delete_list">Liste löschen</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="add_account_description">Aldoni novan Mastodon konton</string>
|
||||||
<string name="action_lists">Listoj</string>
|
<string name="action_lists">Listoj</string>
|
||||||
<string name="title_lists">Listoj</string>
|
<string name="title_lists">Listoj</string>
|
||||||
<string name="title_list_timeline">Tempolinio de la listo</string>
|
|
||||||
<string name="error_create_list">Ne povis krei la liston</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_rename_list">Ne povis ŝanĝi la nomon de la listo</string>
|
||||||
<string name="error_delete_list">Ne povis forigi la liston</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="add_account_description">Añadir cuenta de Mastodon</string>
|
||||||
<string name="action_lists">Listas</string>
|
<string name="action_lists">Listas</string>
|
||||||
<string name="title_lists">Listas</string>
|
<string name="title_lists">Listas</string>
|
||||||
<string name="title_list_timeline">Cronología de lista</string>
|
|
||||||
<string name="compose_active_account_description">Publicando con la cuenta %1$s</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>
|
<string name="error_failed_set_caption">Error al añadir leyenda</string>
|
||||||
<plurals name="hint_describe_for_visually_impaired">
|
<plurals name="hint_describe_for_visually_impaired">
|
||||||
|
|
|
@ -235,7 +235,6 @@
|
||||||
<string name="add_account_description">Mastodon kontua gehitu</string>
|
<string name="add_account_description">Mastodon kontua gehitu</string>
|
||||||
<string name="action_lists">Zerrendak</string>
|
<string name="action_lists">Zerrendak</string>
|
||||||
<string name="title_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="compose_active_account_description">%1$s kontuarekin tut egiten</string>
|
||||||
<string name="error_failed_set_caption">Akatsa deskribapena eranstean</string>
|
<string name="error_failed_set_caption">Akatsa deskribapena eranstean</string>
|
||||||
<plurals name="hint_describe_for_visually_impaired">
|
<plurals name="hint_describe_for_visually_impaired">
|
||||||
|
|
|
@ -229,7 +229,6 @@
|
||||||
<string name="add_account_description">افزودن حساب ماستودون جدید</string>
|
<string name="add_account_description">افزودن حساب ماستودون جدید</string>
|
||||||
<string name="action_lists">فهرستها</string>
|
<string name="action_lists">فهرستها</string>
|
||||||
<string name="title_lists">فهرستها</string>
|
<string name="title_lists">فهرستها</string>
|
||||||
<string name="title_list_timeline">خط زمانی فهرست</string>
|
|
||||||
<string name="compose_active_account_description">در حال فرستادن با حساب %1$s</string>
|
<string name="compose_active_account_description">در حال فرستادن با حساب %1$s</string>
|
||||||
<string name="error_failed_set_caption">شکست در تنظیم عنوان</string>
|
<string name="error_failed_set_caption">شکست در تنظیم عنوان</string>
|
||||||
<plurals name="hint_describe_for_visually_impaired">
|
<plurals name="hint_describe_for_visually_impaired">
|
||||||
|
|
|
@ -275,7 +275,6 @@
|
||||||
<string name="add_account_description">Ajouter un nouveau compte Mastodon</string>
|
<string name="add_account_description">Ajouter un nouveau compte Mastodon</string>
|
||||||
<string name="action_lists">Listes</string>
|
<string name="action_lists">Listes</string>
|
||||||
<string name="title_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_create_list">Impossible de créer la liste</string>
|
||||||
<string name="error_rename_list">Impossible de renommer 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>
|
<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_set_caption">Mettre une légende</string>
|
||||||
<string name="action_remove">Supprimer le média</string>
|
<string name="action_remove">Supprimer le média</string>
|
||||||
<string name="lock_account_label">Verrouiller le compte</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="compose_save_draft">Enregistrer comme brouillon ?</string>
|
||||||
<string name="send_post_notification_title">Envoi du pouet…</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>
|
<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="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="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="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_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_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>
|
<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_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="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="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="no_announcements">Chan eil brath-fios ann.</string>
|
||||||
<string name="title_announcements">Brathan-fios</string>
|
<string name="title_announcements">Brathan-fios</string>
|
||||||
<string name="account_note_saved">Chaidh a shàbhaladh!</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_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_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="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_description">Cuir cunntas Mastodon ùr ris</string>
|
||||||
<string name="add_account_name">Cuir cunntas ris</string>
|
<string name="add_account_name">Cuir cunntas ris</string>
|
||||||
<string name="filter_add_description">An abairt ri chriathradh</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_schedule_post">Cuir post air an sgeideal</string>
|
||||||
<string name="action_toggle_visibility">Faicsinneachd a’ phuist</string>
|
<string name="action_toggle_visibility">Faicsinneachd a’ phuist</string>
|
||||||
<string name="action_access_scheduled_posts">Postaichean air an sgeideal</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="notification_reblog_format">Bhrosnaich %s am post agad</string>
|
||||||
<string name="title_scheduled_posts">Postaichean air an sgeideal</string>
|
<string name="title_scheduled_posts">Postaichean air an sgeideal</string>
|
||||||
<string name="title_view_thread">Snàithlean</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="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="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="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>
|
</resources>
|
|
@ -275,7 +275,6 @@
|
||||||
<string name="error_delete_list">Non se puido eliminar a listaxe</string>
|
<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_rename_list">Non se puido renomear a listaxe</string>
|
||||||
<string name="error_create_list">Non se puido crear 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="title_lists">Listaxes</string>
|
||||||
<string name="action_lists">Listaxes</string>
|
<string name="action_lists">Listaxes</string>
|
||||||
<string name="add_account_description">Engadir unha nova conta Mastodon</string>
|
<string name="add_account_description">Engadir unha nova conta Mastodon</string>
|
||||||
|
|
|
@ -248,7 +248,6 @@
|
||||||
<string name="compose_save_draft">लिखने को सुरक्षित करें\?</string>
|
<string name="compose_save_draft">लिखने को सुरक्षित करें\?</string>
|
||||||
<string name="lock_account_label">खाता लॉक करें</string>
|
<string name="lock_account_label">खाता लॉक करें</string>
|
||||||
<string name="action_set_caption">कैप्शन सेट करें</string>
|
<string name="action_set_caption">कैप्शन सेट करें</string>
|
||||||
<string name="title_list_timeline">सूची टाइमलाइन</string>
|
|
||||||
<string name="add_account_name">खाता जोड़ो</string>
|
<string name="add_account_name">खाता जोड़ो</string>
|
||||||
<string name="filter_dialog_whole_word">पूरा शब्द</string>
|
<string name="filter_dialog_whole_word">पूरा शब्द</string>
|
||||||
<string name="filter_edit_dialog_title">फ़िल्टर संपादित करें</string>
|
<string name="filter_edit_dialog_title">फ़िल्टर संपादित करें</string>
|
||||||
|
|
|
@ -336,7 +336,6 @@
|
||||||
<string name="abbreviated_in_seconds">%dmp múlva</string>
|
<string name="abbreviated_in_seconds">%dmp múlva</string>
|
||||||
<string name="filter_dialog_whole_word">Teljes szó</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="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="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_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>
|
<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="filter_add_description">Frasi sem á að sía</string>
|
||||||
<string name="add_account_name">Bæta við aðgang</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="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_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_rename_list">Ekki tókst að endurnefna lista</string>
|
||||||
<string name="error_delete_list">Ekki tókst að eyða 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="add_account_description">Aggiungi un nuovo Account Mastodon</string>
|
||||||
<string name="action_lists">Liste</string>
|
<string name="action_lists">Liste</string>
|
||||||
<string name="title_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_create_list">Non è stato possibile creare la lista</string>
|
||||||
<string name="error_rename_list">Non è stato possibile rinominare 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>
|
<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="add_account_description">新しいMastodonアカウントを追加</string>
|
||||||
<string name="action_lists">リスト</string>
|
<string name="action_lists">リスト</string>
|
||||||
<string name="title_lists">リスト</string>
|
<string name="title_lists">リスト</string>
|
||||||
<string name="title_list_timeline">リストタイムライン</string>
|
|
||||||
<string name="error_rename_list">リスト名を変更できませんでした</string>
|
<string name="error_rename_list">リスト名を変更できませんでした</string>
|
||||||
<string name="action_rename_list">リスト名の変更</string>
|
<string name="action_rename_list">リスト名の変更</string>
|
||||||
<string name="compose_active_account_description">%1$sで投稿</string>
|
<string name="compose_active_account_description">%1$sで投稿</string>
|
||||||
|
|
|
@ -282,7 +282,6 @@
|
||||||
<string name="add_account_description">마스토돈 계정을 추가합니다</string>
|
<string name="add_account_description">마스토돈 계정을 추가합니다</string>
|
||||||
<string name="action_lists">리스트</string>
|
<string name="action_lists">리스트</string>
|
||||||
<string name="title_lists">리스트</string>
|
<string name="title_lists">리스트</string>
|
||||||
<string name="title_list_timeline">리스트 타임라인</string>
|
|
||||||
<string name="error_create_list">리스트를 만들 수 없습니다.</string>
|
<string name="error_create_list">리스트를 만들 수 없습니다.</string>
|
||||||
<string name="error_rename_list">리스트의 이름을 변경할 수 없습니다.</string>
|
<string name="error_rename_list">리스트의 이름을 변경할 수 없습니다.</string>
|
||||||
<string name="error_delete_list">리스트를 삭제할 수 없습니다.</string>
|
<string name="error_delete_list">리스트를 삭제할 수 없습니다.</string>
|
||||||
|
|
|
@ -259,7 +259,6 @@
|
||||||
<string name="add_account_description">Een nieuw Mastodonaccount toevoegen</string>
|
<string name="add_account_description">Een nieuw Mastodonaccount toevoegen</string>
|
||||||
<string name="action_lists">Lijsten</string>
|
<string name="action_lists">Lijsten</string>
|
||||||
<string name="title_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="compose_active_account_description">Aan het publiceren met account %1$s</string>
|
||||||
<string name="error_failed_set_caption">Toevoegen van beschrijving mislukt</string>
|
<string name="error_failed_set_caption">Toevoegen van beschrijving mislukt</string>
|
||||||
<plurals name="hint_describe_for_visually_impaired">
|
<plurals name="hint_describe_for_visually_impaired">
|
||||||
|
|
|
@ -242,7 +242,6 @@
|
||||||
<string name="add_account_description">Legg til ny Mastodon-konto</string>
|
<string name="add_account_description">Legg til ny Mastodon-konto</string>
|
||||||
<string name="action_lists">Lister</string>
|
<string name="action_lists">Lister</string>
|
||||||
<string name="title_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_create_list">Kunne ikke opprette liste</string>
|
||||||
<string name="error_rename_list">Kunne ikke gi liste nytt navn</string>
|
<string name="error_rename_list">Kunne ikke gi liste nytt navn</string>
|
||||||
<string name="error_delete_list">Kunne ikke slette liste</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="add_account_description">Apondre un nòu compte Mastodon</string>
|
||||||
<string name="action_lists">Listas</string>
|
<string name="action_lists">Listas</string>
|
||||||
<string name="title_lists">Listas</string>
|
<string name="title_lists">Listas</string>
|
||||||
<string name="title_list_timeline">Flux de la lista</string>
|
|
||||||
<string name="compose_active_account_description">Publicar amb lo compte %1$s</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="error_failed_set_caption">Fracàs en apondre una legenda</string>
|
||||||
<string name="action_set_caption">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_home">Strona główna</string>
|
||||||
<string name="title_notifications">Powiadomienia</string>
|
<string name="title_notifications">Powiadomienia</string>
|
||||||
<string name="title_public_local">Lokalne</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_view_thread">Wątek</string>
|
||||||
<string name="title_posts">Wpisy</string>
|
<string name="title_posts">Wpisy</string>
|
||||||
<string name="title_posts_with_replies">Z odpowiedziami</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_edit_profile">Edytuj profil</string>
|
||||||
<string name="title_drafts">Szkice</string>
|
<string name="title_drafts">Szkice</string>
|
||||||
<string name="title_licenses">Licencje</string>
|
<string name="title_licenses">Licencje</string>
|
||||||
<string name="post_boosted_format">%s podbił</string>
|
<string name="post_boosted_format">%s podbite</string>
|
||||||
<string name="post_sensitive_media_title">Wrażliwe treści</string>
|
<string name="post_sensitive_media_title">Treści wrażliwe</string>
|
||||||
<string name="post_media_hidden_title">Ukryto zawartość multimedialną</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_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_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="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_reblog_format">%s podbił(-a) Twój wpis</string>
|
||||||
<string name="notification_favourite_format">%s dodał Twój post do ulubionych</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="action_emoji_keyboard">Klawiatura emoji</string>
|
||||||
<string name="download_image">Pobieranie %1$s</string>
|
<string name="download_image">Pobieranie %1$s</string>
|
||||||
<string name="action_copy_link">Skopiuj odnośnik</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="send_post_content_to">Udostępnij wpis do…</string>
|
||||||
<string name="confirmation_reported">Wyślij!</string>
|
<string name="confirmation_reported">Wyślij!</string>
|
||||||
<string name="confirmation_unblocked">Odblokowano użytkownika</string>
|
<string name="confirmation_unblocked">Odblokowano użytkownika</string>
|
||||||
<string name="confirmation_unmuted">Cofnięto wyciszenie użytkownika</string>
|
<string name="confirmation_unmuted">Cofnięto wyciszenie użytkownika</string>
|
||||||
<string name="post_sent">Wyślij!</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_domain">Jaka instancja?</string>
|
||||||
<string name="hint_compose">Co Ci chodzi po głowie?</string>
|
<string name="hint_compose">Co Ci chodzi po głowie?</string>
|
||||||
<string name="hint_content_warning">Ostrzeżenie o zawartości</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_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_hide_follow_button">Ukryj przycisk śledzenia podczas przewijania</string>
|
||||||
<string name="pref_title_post_filter">Filtrowanie osi czasu</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_boosts">Pokaż podbicia</string>
|
||||||
<string name="pref_title_show_replies">Pokazuj odpowiedzi</string>
|
<string name="pref_title_show_replies">Pokazuj odpowiedzi</string>
|
||||||
<string name="pref_title_show_media_preview">Pokazuj podgląd zawartości multimedialnej</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_medium">%1$s, %2$s, i %3$s</string>
|
||||||
<string name="notification_summary_small">%1$s i %2$s</string>
|
<string name="notification_summary_small">%1$s i %2$s</string>
|
||||||
<plurals name="notification_title_summary">
|
<plurals name="notification_title_summary">
|
||||||
<item quantity="one">%d nowe powiadomienie</item>
|
<item quantity="one">%d nowa interakcja</item>
|
||||||
<item quantity="few">%d nowe powiadomienia</item>
|
<item quantity="few">%d nowe interakcje</item>
|
||||||
<item quantity="many">%d nowych powiadomień</item>
|
<item quantity="many">%d nowych interakcji</item>
|
||||||
<item quantity="other">%d nowych powiadomień</item>
|
<item quantity="other">%d nowych interakcji</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="description_account_locked">Konto zablokowane</string>
|
<string name="description_account_locked">Konto zablokowane</string>
|
||||||
<string name="about_title_activity">O programie</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="add_account_description">Dodaj nowe Konto Mastodon</string>
|
||||||
<string name="action_lists">Listy</string>
|
<string name="action_lists">Listy</string>
|
||||||
<string name="title_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="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="error_failed_set_caption">Nie udało się ustawić podpisu</string>
|
||||||
<string name="action_set_caption">Ustaw podpis</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_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>
|
<string name="poll_ended_created">Ankieta, którą stworzyłeś(aś), zakończyła się</string>
|
||||||
<plurals name="poll_timespan_days">
|
<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="few">Zostało %d dni</item>
|
||||||
<item quantity="many">Zostało %d dni</item>
|
<item quantity="many">Zostało %d dni</item>
|
||||||
<item quantity="other">Zostało %d dni</item>
|
<item quantity="other">Zostało %d dni</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="poll_timespan_hours">
|
<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="few">Zostało %d godziny</item>
|
||||||
<item quantity="many">Zostało %d godzin</item>
|
<item quantity="many">Zostało %d godzin</item>
|
||||||
<item quantity="other">Zostało %d godzin</item>
|
<item quantity="other">Zostało %d godzin</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="poll_timespan_minutes">
|
<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="few">Zostało %d minuty</item>
|
||||||
<item quantity="many">Zostało %d minut</item>
|
<item quantity="many">Zostało %d minut</item>
|
||||||
<item quantity="other">Zostało %d minut</item>
|
<item quantity="other">Zostało %d minut</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="poll_timespan_seconds">
|
<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="few">Zostało %d sekund</item>
|
||||||
<item quantity="many">Zostało %d sekund</item>
|
<item quantity="many">Zostało %d sekund</item>
|
||||||
<item quantity="other">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="title_bookmarks">Zakładki</string>
|
||||||
<string name="action_bookmark">Dodaj do zakładek</string>
|
<string name="action_bookmark">Dodaj do zakładek</string>
|
||||||
<string name="action_view_bookmarks">Zakładki</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="select_list_title">Wybierz listę</string>
|
||||||
<string name="list">Lista</string>
|
<string name="list">Lista</string>
|
||||||
<string name="error_audio_upload_size">Pliki audio muszą być mniejsze niż 40MB.</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_bottom">Dół</string>
|
||||||
<string name="pref_main_nav_position_option_top">Góra</string>
|
<string name="pref_main_nav_position_option_top">Góra</string>
|
||||||
<plurals name="error_upload_max_media_reached">
|
<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="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łączników.</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="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>
|
<item quantity="other">Nie możesz przesłać więcej niż %1$d załączników.</item>
|
||||||
</plurals>
|
</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="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="post_media_attachments">Załączniki</string>
|
||||||
<string name="notification_follow_request_description">Powiadomienia o prośbach o obserwowanie</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="pref_title_notification_filter_follow_requests">Wysłano prośbę o obserwowanie</string>
|
||||||
<string name="title_announcements">Ogłoszenia</string>
|
<string name="title_announcements">Ogłoszenia</string>
|
||||||
<string name="pref_title_wellbeing_mode">Zdrowie</string>
|
<string name="pref_title_wellbeing_mode">Zdrowie</string>
|
||||||
<string name="action_unsubscribe_account">Anuluj subskrypcję</string>
|
<string name="action_unsubscribe_account">Anuluj subskrypcję</string>
|
||||||
<string name="action_subscribe_account">Zasubskrybuj</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="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="draft_deleted">Usunięto szkic</string>
|
||||||
<string name="wellbeing_hide_stats_profile">Ukryj ilościowe statystyki na profilach</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="wellbeing_hide_stats_posts">Ukryj ilościowe statystyki na postach</string>
|
||||||
<string name="review_notifications">Przejrzyj powiadomienia</string>
|
<string name="review_notifications">Przejrzyj powiadomienia</string>
|
||||||
<string name="account_note_saved">Zapisano!</string>
|
<string name="account_note_saved">Zapisano!</string>
|
||||||
<string name="account_note_hint">Twoja prywatna notatka o tym koncie</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="post_media_audio">Dźwięk</string>
|
||||||
<string name="notification_subscription_description">Powiadomienia o opublikowaniu nowego wpisu przez kogoś, kogo obserwujesz</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>
|
<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_format">%s zarejestrował(a) się</string>
|
||||||
<string name="notification_sign_up_name">Rejestracje</string>
|
<string name="notification_sign_up_name">Rejestracje</string>
|
||||||
<string name="notification_sign_up_description">Powiadomienia o nowych użytkownikach</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_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_format">%s edytował(a) swój wpis</string>
|
||||||
<string name="notification_update_name">Edycje wpisów</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>
|
</resources>
|
|
@ -246,7 +246,6 @@
|
||||||
<string name="add_account_description">Adicionar nova conta Mastodon</string>
|
<string name="add_account_description">Adicionar nova conta Mastodon</string>
|
||||||
<string name="action_lists">Listas</string>
|
<string name="action_lists">Listas</string>
|
||||||
<string name="title_lists">Listas</string>
|
<string name="title_lists">Listas</string>
|
||||||
<string name="title_list_timeline">Linha da lista</string>
|
|
||||||
<string name="compose_active_account_description">Usando a conta %1$s</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="error_failed_set_caption">Erro ao incluir descrição</string>
|
||||||
<string name="action_set_caption">Descrever</string>
|
<string name="action_set_caption">Descrever</string>
|
||||||
|
|
|
@ -325,7 +325,7 @@
|
||||||
<string name="action_lists">Listas</string>
|
<string name="action_lists">Listas</string>
|
||||||
<string name="error_rename_list">Não foi possível renomear a lista</string>
|
<string name="error_rename_list">Não foi possível renomear a lista</string>
|
||||||
<string name="title_lists">Listas</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_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="error_delete_list">Não foi possível apagar a lista</string>
|
||||||
<string name="action_create_list">Criar uma lista</string>
|
<string name="action_create_list">Criar uma lista</string>
|
||||||
|
|
|
@ -296,7 +296,6 @@
|
||||||
<string name="add_account_description">Добавить новый акканут Mastodon</string>
|
<string name="add_account_description">Добавить новый акканут Mastodon</string>
|
||||||
<string name="action_lists">Списки</string>
|
<string name="action_lists">Списки</string>
|
||||||
<string name="title_lists">Списки</string>
|
<string name="title_lists">Списки</string>
|
||||||
<string name="title_list_timeline">Список лент</string>
|
|
||||||
<string name="error_create_list">Не удалось создать список</string>
|
<string name="error_create_list">Не удалось создать список</string>
|
||||||
<string name="error_rename_list">Не удалось переименовать список</string>
|
<string name="error_rename_list">Не удалось переименовать список</string>
|
||||||
<string name="error_delete_list">Не удалось удалить список</string>
|
<string name="error_delete_list">Не удалось удалить список</string>
|
||||||
|
|
|
@ -202,7 +202,6 @@
|
||||||
<string name="error_rename_list">पुनः सूचिनामकरणं कर्तुमशक्यम्</string>
|
<string name="error_rename_list">पुनः सूचिनामकरणं कर्तुमशक्यम्</string>
|
||||||
<string name="error_create_list">सूचिनिर्माणं कर्तुमशक्यम्</string>
|
<string name="error_create_list">सूचिनिर्माणं कर्तुमशक्यम्</string>
|
||||||
<string name="dialog_message_cancel_follow_request">अनुसरणानुरोधो नश्यताम् \?</string>
|
<string name="dialog_message_cancel_follow_request">अनुसरणानुरोधो नश्यताम् \?</string>
|
||||||
<string name="title_list_timeline">सूचेः समयतालिका</string>
|
|
||||||
<string name="title_lists">सूचयः</string>
|
<string name="title_lists">सूचयः</string>
|
||||||
<string name="action_lists">सूचयः</string>
|
<string name="action_lists">सूचयः</string>
|
||||||
<string name="add_account_description">नवमास्टोडोनलेखा युज्यताम्</string>
|
<string name="add_account_description">नवमास्टोडोनलेखा युज्यताम्</string>
|
||||||
|
|
|
@ -247,7 +247,6 @@
|
||||||
<string name="add_account_description">Dodaj nov Mastodon račun</string>
|
<string name="add_account_description">Dodaj nov Mastodon račun</string>
|
||||||
<string name="action_lists">Seznami</string>
|
<string name="action_lists">Seznami</string>
|
||||||
<string name="title_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_create_list">Seznama ni bilo mogoče ustvariti</string>
|
||||||
<string name="error_rename_list">Seznama ni bilo mogoče preimenovati</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>
|
<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="add_account_description">Lägg till ett nytt Mastodon-konto</string>
|
||||||
<string name="action_lists">Listor</string>
|
<string name="action_lists">Listor</string>
|
||||||
<string name="title_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_create_list">Kunde inte skapa lista</string>
|
||||||
<string name="error_rename_list">Kunde inte byta namn på lista</string>
|
<string name="error_rename_list">Kunde inte byta namn på lista</string>
|
||||||
<string name="error_delete_list">Kunde inte radera 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="add_account_description">புதிய Mastodon கணக்கைச் சேர்க்க</string>
|
||||||
<string name="action_lists">பட்டியல்கள்</string>
|
<string name="action_lists">பட்டியல்கள்</string>
|
||||||
<string name="title_lists">பட்டியல்கள்</string>
|
<string name="title_lists">பட்டியல்கள்</string>
|
||||||
<string name="title_list_timeline">காலவரிசை பட்டியல்</string>
|
|
||||||
<string name="compose_active_account_description">%1$s கணக்குடன் பதிவிட</string>
|
<string name="compose_active_account_description">%1$s கணக்குடன் பதிவிட</string>
|
||||||
<string name="error_failed_set_caption">தலைப்பை அமைக்க முடியவில்லை</string>
|
<string name="error_failed_set_caption">தலைப்பை அமைக்க முடியவில்லை</string>
|
||||||
<string name="action_set_caption">தலைப்பை அமை</string>
|
<string name="action_set_caption">தலைப்பை அமை</string>
|
||||||
|
|
|
@ -144,7 +144,6 @@
|
||||||
<string name="error_delete_list">ไม่สามารถลบรายการได้</string>
|
<string name="error_delete_list">ไม่สามารถลบรายการได้</string>
|
||||||
<string name="error_rename_list">ไม่สามารถเปลี่ยนชื่อรายการได้</string>
|
<string name="error_rename_list">ไม่สามารถเปลี่ยนชื่อรายการได้</string>
|
||||||
<string name="error_create_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_description">เพิ่มบัญชี Mastodon ใหม่</string>
|
||||||
<string name="add_account_name">เพิ่มบัญชี</string>
|
<string name="add_account_name">เพิ่มบัญชี</string>
|
||||||
<string name="filter_add_description">วลีที่ต้องการกรอง</string>
|
<string name="filter_add_description">วลีที่ต้องการกรอง</string>
|
||||||
|
|
|
@ -243,7 +243,6 @@
|
||||||
<string name="add_account_description">Yeni Mastodon hesabı ekle</string>
|
<string name="add_account_description">Yeni Mastodon hesabı ekle</string>
|
||||||
<string name="action_lists">Listeler</string>
|
<string name="action_lists">Listeler</string>
|
||||||
<string name="title_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>
|
<string name="compose_active_account_description">%1$s hesabıyla gönderiliyor</string>
|
||||||
<plurals name="hint_describe_for_visually_impaired">
|
<plurals name="hint_describe_for_visually_impaired">
|
||||||
<item quantity="other">Görsel engelli için tanımla
|
<item quantity="other">Görsel engelli için tanımla
|
||||||
|
|
|
@ -285,7 +285,6 @@
|
||||||
<string name="error_delete_list">Не вдалося видалити список</string>
|
<string name="error_delete_list">Не вдалося видалити список</string>
|
||||||
<string name="error_rename_list">Не вдалося перейменувати список</string>
|
<string name="error_rename_list">Не вдалося перейменувати список</string>
|
||||||
<string name="error_create_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_description">Додати новий обліковий запис Mastodon</string>
|
||||||
<string name="add_account_name">Додати обліковий запис</string>
|
<string name="add_account_name">Додати обліковий запис</string>
|
||||||
<string name="filter_add_description">Фільтрувати фразу</string>
|
<string name="filter_add_description">Фільтрувати фразу</string>
|
||||||
|
@ -550,4 +549,5 @@
|
||||||
<string name="notification_update_name">Редакції допису</string>
|
<string name="notification_update_name">Редакції допису</string>
|
||||||
<string name="title_login">Вхід</string>
|
<string name="title_login">Вхід</string>
|
||||||
<string name="error_could_not_load_login_page">Не вдалося завантажити сторінку входу.</string>
|
<string name="error_could_not_load_login_page">Не вдалося завантажити сторінку входу.</string>
|
||||||
|
<string name="saving_draft">Збереження чернетки…</string>
|
||||||
</resources>
|
</resources>
|
|
@ -448,7 +448,6 @@
|
||||||
<string name="action_delete_list">Xóa danh sách</string>
|
<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_rename_list">Đổi tên danh sách</string>
|
||||||
<string name="action_create_list">Tạo 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_description">Thêm tài khoản Mastodon</string>
|
||||||
<string name="add_account_name">Thêm tài khoản</string>
|
<string name="add_account_name">Thêm tài khoản</string>
|
||||||
<string name="filter_add_description">Thêm mô tả</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="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="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="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>
|
</resources>
|
|
@ -277,7 +277,6 @@
|
||||||
<string name="add_account_description">添加新的 Mastodon 帐号</string>
|
<string name="add_account_description">添加新的 Mastodon 帐号</string>
|
||||||
<string name="action_lists">列表</string>
|
<string name="action_lists">列表</string>
|
||||||
<string name="title_lists">列表</string>
|
<string name="title_lists">列表</string>
|
||||||
<string name="title_list_timeline">列表时间轴</string>
|
|
||||||
<string name="error_create_list">无法新建列表</string>
|
<string name="error_create_list">无法新建列表</string>
|
||||||
<string name="error_rename_list">无法重命名列表</string>
|
<string name="error_rename_list">无法重命名列表</string>
|
||||||
<string name="error_delete_list">无法删除列表</string>
|
<string name="error_delete_list">无法删除列表</string>
|
||||||
|
@ -536,4 +535,5 @@
|
||||||
<string name="notification_update_name">嘟文编辑</string>
|
<string name="notification_update_name">嘟文编辑</string>
|
||||||
<string name="notification_update_description">当你进行过互动的嘟文被编辑时发出通知</string>
|
<string name="notification_update_description">当你进行过互动的嘟文被编辑时发出通知</string>
|
||||||
<string name="error_could_not_load_login_page">无法加载登录页。</string>
|
<string name="error_could_not_load_login_page">无法加载登录页。</string>
|
||||||
|
<string name="saving_draft">正在保存草稿…</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -276,7 +276,6 @@
|
||||||
<string name="add_account_description">加入新的 Mastodon 帳號</string>
|
<string name="add_account_description">加入新的 Mastodon 帳號</string>
|
||||||
<string name="action_lists">列表</string>
|
<string name="action_lists">列表</string>
|
||||||
<string name="title_lists">列表</string>
|
<string name="title_lists">列表</string>
|
||||||
<string name="title_list_timeline">列表時間軸</string>
|
|
||||||
<string name="error_create_list">無法新建列表</string>
|
<string name="error_create_list">無法新建列表</string>
|
||||||
<string name="error_rename_list">無法重命名列表</string>
|
<string name="error_rename_list">無法重命名列表</string>
|
||||||
<string name="error_delete_list">無法刪除列表</string>
|
<string name="error_delete_list">無法刪除列表</string>
|
||||||
|
|
|
@ -270,7 +270,6 @@
|
||||||
<string name="add_account_description">加入新的 Mastodon 帳號</string>
|
<string name="add_account_description">加入新的 Mastodon 帳號</string>
|
||||||
<string name="action_lists">列表</string>
|
<string name="action_lists">列表</string>
|
||||||
<string name="title_lists">列表</string>
|
<string name="title_lists">列表</string>
|
||||||
<string name="title_list_timeline">列表時間軸</string>
|
|
||||||
<string name="error_create_list">無法新建列表</string>
|
<string name="error_create_list">無法新建列表</string>
|
||||||
<string name="error_rename_list">無法重命名列表</string>
|
<string name="error_rename_list">無法重命名列表</string>
|
||||||
<string name="error_delete_list">無法刪除列表</string>
|
<string name="error_delete_list">無法刪除列表</string>
|
||||||
|
|
|
@ -274,7 +274,6 @@
|
||||||
<string name="add_account_description">添加新的 Mastodon 帐号</string>
|
<string name="add_account_description">添加新的 Mastodon 帐号</string>
|
||||||
<string name="action_lists">列表</string>
|
<string name="action_lists">列表</string>
|
||||||
<string name="title_lists">列表</string>
|
<string name="title_lists">列表</string>
|
||||||
<string name="title_list_timeline">列表时间轴</string>
|
|
||||||
<string name="error_create_list">无法新建列表</string>
|
<string name="error_create_list">无法新建列表</string>
|
||||||
<string name="error_rename_list">无法重命名列表</string>
|
<string name="error_rename_list">无法重命名列表</string>
|
||||||
<string name="error_delete_list">无法删除列表</string>
|
<string name="error_delete_list">无法删除列表</string>
|
||||||
|
|
|
@ -276,7 +276,6 @@
|
||||||
<string name="add_account_description">加入新的 Mastodon 帳號</string>
|
<string name="add_account_description">加入新的 Mastodon 帳號</string>
|
||||||
<string name="action_lists">列表</string>
|
<string name="action_lists">列表</string>
|
||||||
<string name="title_lists">列表</string>
|
<string name="title_lists">列表</string>
|
||||||
<string name="title_list_timeline">列表時間軸</string>
|
|
||||||
<string name="error_create_list">無法新建列表</string>
|
<string name="error_create_list">無法新建列表</string>
|
||||||
<string name="error_rename_list">無法重命名列表</string>
|
<string name="error_rename_list">無法重命名列表</string>
|
||||||
<string name="error_delete_list">無法刪除列表</string>
|
<string name="error_delete_list">無法刪除列表</string>
|
||||||
|
@ -525,4 +524,6 @@
|
||||||
<string name="pref_title_alway_open_spoiler">總是顯示被標注為內容警告的嘟文</string>
|
<string name="pref_title_alway_open_spoiler">總是顯示被標注為內容警告的嘟文</string>
|
||||||
<string name="failed_search">搜尋失敗</string>
|
<string name="failed_search">搜尋失敗</string>
|
||||||
<string name="title_accounts">帳號</string>
|
<string name="title_accounts">帳號</string>
|
||||||
|
<string name="title_login">登入</string>
|
||||||
|
<string name="error_could_not_load_login_page">無法載入登入頁面。</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -44,6 +44,7 @@
|
||||||
<string name="title_mutes">Muted users</string>
|
<string name="title_mutes">Muted users</string>
|
||||||
<string name="title_blocks">Blocked users</string>
|
<string name="title_blocks">Blocked users</string>
|
||||||
<string name="title_domain_mutes">Hidden domains</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_follow_requests">Follow Requests</string>
|
||||||
<string name="title_edit_profile">Edit your profile</string>
|
<string name="title_edit_profile">Edit your profile</string>
|
||||||
<string name="title_drafts">Drafts</string>
|
<string name="title_drafts">Drafts</string>
|
||||||
|
@ -152,6 +153,8 @@
|
||||||
<string name="action_open_reblogger">Open boost author</string>
|
<string name="action_open_reblogger">Open boost author</string>
|
||||||
<string name="action_open_reblogged_by">Show boosts</string>
|
<string name="action_open_reblogged_by">Show boosts</string>
|
||||||
<string name="action_open_faved_by">Show favorites</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_quote">Quote</string>
|
||||||
<string name="action_authorize">Authorize Now!</string>
|
<string name="action_authorize">Authorize Now!</string>
|
||||||
<string name="action_tab_jump_to_top">Jump to top</string>
|
<string name="action_tab_jump_to_top">Jump to top</string>
|
||||||
|
@ -413,7 +416,6 @@
|
||||||
|
|
||||||
<string name="action_lists">Lists</string>
|
<string name="action_lists">Lists</string>
|
||||||
<string name="title_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_create_list">Could not create list</string>
|
||||||
<string name="error_rename_list">Could not rename list</string>
|
<string name="error_rename_list">Could not rename list</string>
|
||||||
<string name="error_delete_list">Could not delete 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="action_unsubscribe_account">Unsubscribe</string>
|
||||||
|
|
||||||
<string name="tusky_compose_post_quicksetting_label">Compose Post</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="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>
|
</resources>
|
||||||
|
|
|
@ -47,6 +47,8 @@ import org.robolectric.Robolectric
|
||||||
import org.robolectric.Shadows.shadowOf
|
import org.robolectric.Shadows.shadowOf
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
import org.robolectric.fakes.RoboMenuItem
|
import org.robolectric.fakes.RoboMenuItem
|
||||||
|
import java.util.Date
|
||||||
|
import kotlin.collections.HashMap
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by charlag on 3/7/18.
|
* Created by charlag on 3/7/18.
|
||||||
|
@ -466,22 +468,23 @@ class ComposeActivityTest {
|
||||||
null,
|
null,
|
||||||
listOf("en"),
|
listOf("en"),
|
||||||
Account(
|
Account(
|
||||||
"1",
|
id = "1",
|
||||||
"admin",
|
localUsername = "admin",
|
||||||
"admin",
|
username = "admin",
|
||||||
"admin",
|
displayName = "admin",
|
||||||
"",
|
createdAt = Date(),
|
||||||
"https://example.token",
|
note = "",
|
||||||
"",
|
url = "https://example.token",
|
||||||
"",
|
avatar = "",
|
||||||
false,
|
header = "",
|
||||||
0,
|
locked = false,
|
||||||
0,
|
statusesCount = 0,
|
||||||
0,
|
followersCount = 0,
|
||||||
null,
|
followingCount = 0,
|
||||||
false,
|
source = null,
|
||||||
emptyList(),
|
bot = false,
|
||||||
emptyList()
|
emojis = emptyList(),
|
||||||
|
fields = emptyList(),
|
||||||
),
|
),
|
||||||
maximumLegacyTootCharacters,
|
maximumLegacyTootCharacters,
|
||||||
null,
|
null,
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky
|
package com.keylesspalace.tusky
|
||||||
|
|
||||||
import com.keylesspalace.tusky.util.ComposeTokenizer
|
import com.keylesspalace.tusky.components.compose.ComposeTokenizer
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
|
@ -5,8 +5,8 @@ buildscript {
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath "com.android.tools.build:gradle:7.1.2"
|
classpath "com.android.tools.build:gradle:7.2.0"
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.20"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21"
|
||||||
classpath "org.jlleitschuh.gradle:ktlint-gradle:10.2.1"
|
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