mirror of https://github.com/tuskyapp/Tusky.git
Browse Source
* Migrate LinkHelper to kotlin * Support tags field on statuses * Use embedded tags list in status instead of text scraping to embed tag click handler. Fixes #2283 * Make mentions and tags lists nonnullable * Make LinkHelper.openLink a Context extension method * Use builtin extension for uri conversion * More cleanup in LinkHelper * Add tests for LinkHelper.getDomain * Unbreak tags in places that don't have a tag list (e.g. profiles) * Fixup javadocpull/2354/head
34 changed files with 1294 additions and 296 deletions
@ -0,0 +1,789 @@
|
||||
{ |
||||
"formatVersion": 1, |
||||
"database": { |
||||
"version": 29, |
||||
"identityHash": "d3643e2bf6d8a2efb13254a0ea3ab2a1", |
||||
"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, `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)", |
||||
"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": "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 |
||||
} |
||||
], |
||||
"primaryKey": { |
||||
"columnNames": [ |
||||
"id" |
||||
], |
||||
"autoGenerate": true |
||||
}, |
||||
"indices": [ |
||||
{ |
||||
"name": "index_AccountEntity_domain_accountId", |
||||
"unique": true, |
||||
"columnNames": [ |
||||
"domain", |
||||
"accountId" |
||||
], |
||||
"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, `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": "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, 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 |
||||
} |
||||
], |
||||
"primaryKey": { |
||||
"columnNames": [ |
||||
"serverId", |
||||
"timelineUserId" |
||||
], |
||||
"autoGenerate": false |
||||
}, |
||||
"indices": [ |
||||
{ |
||||
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId", |
||||
"unique": false, |
||||
"columnNames": [ |
||||
"authorServerId", |
||||
"timelineUserId" |
||||
], |
||||
"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 NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` 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": true |
||||
}, |
||||
{ |
||||
"fieldPath": "lastStatus.showingHiddenContent", |
||||
"columnName": "s_showingHiddenContent", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "lastStatus.expanded", |
||||
"columnName": "s_expanded", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "lastStatus.collapsible", |
||||
"columnName": "s_collapsible", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "lastStatus.collapsed", |
||||
"columnName": "s_collapsed", |
||||
"affinity": "INTEGER", |
||||
"notNull": true |
||||
}, |
||||
{ |
||||
"fieldPath": "lastStatus.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, 'd3643e2bf6d8a2efb13254a0ea3ab2a1')" |
||||
] |
||||
} |
||||
} |
||||
@ -1,3 +1,3 @@
|
||||
package com.keylesspalace.tusky.entity |
||||
|
||||
data class HashTag(val name: String) |
||||
data class HashTag(val name: String, val url: String) |
||||
|
||||
@ -1,251 +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.util; |
||||
|
||||
import android.content.ActivityNotFoundException; |
||||
import android.content.Context; |
||||
import android.content.Intent; |
||||
import android.net.Uri; |
||||
import android.text.SpannableStringBuilder; |
||||
import android.text.Spanned; |
||||
import android.text.method.LinkMovementMethod; |
||||
import android.text.style.ClickableSpan; |
||||
import android.text.style.URLSpan; |
||||
import android.util.Log; |
||||
import android.view.View; |
||||
import android.widget.TextView; |
||||
|
||||
import androidx.annotation.NonNull; |
||||
import androidx.annotation.Nullable; |
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams; |
||||
import androidx.browser.customtabs.CustomTabsIntent; |
||||
import androidx.preference.PreferenceManager; |
||||
|
||||
import com.keylesspalace.tusky.R; |
||||
import com.keylesspalace.tusky.entity.Status; |
||||
import com.keylesspalace.tusky.interfaces.LinkListener; |
||||
|
||||
import java.net.URI; |
||||
import java.net.URISyntaxException; |
||||
import java.util.List; |
||||
|
||||
public class LinkHelper { |
||||
public static String getDomain(String urlString) { |
||||
URI uri; |
||||
try { |
||||
uri = new URI(urlString); |
||||
} catch (URISyntaxException e) { |
||||
return ""; |
||||
} |
||||
String host = uri.getHost(); |
||||
if(host == null) { |
||||
return ""; |
||||
} else if (host.startsWith("www.")) { |
||||
return host.substring(4); |
||||
} else { |
||||
return host; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Finds links, mentions, and hashtags in a piece of text and makes them clickable, associating |
||||
* them with callbacks to notify when they're clicked. |
||||
* |
||||
* @param view the returned text will be put in |
||||
* @param content containing text with mentions, links, or hashtags |
||||
* @param mentions any '@' mentions which are known to be in the content |
||||
* @param listener to notify about particular spans that are clicked |
||||
*/ |
||||
public static void setClickableText(TextView view, CharSequence content, |
||||
@Nullable List<Status.Mention> mentions, final LinkListener listener) { |
||||
SpannableStringBuilder builder = SpannableStringBuilder.valueOf(content); |
||||
URLSpan[] urlSpans = builder.getSpans(0, content.length(), URLSpan.class); |
||||
for (URLSpan span : urlSpans) { |
||||
int start = builder.getSpanStart(span); |
||||
int end = builder.getSpanEnd(span); |
||||
int flags = builder.getSpanFlags(span); |
||||
CharSequence text = builder.subSequence(start, end); |
||||
ClickableSpan customSpan = null; |
||||
|
||||
if (text.charAt(0) == '#') { |
||||
final String tag = text.subSequence(1, text.length()).toString(); |
||||
customSpan = new NoUnderlineURLSpan(span.getURL()) { |
||||
@Override |
||||
public void onClick(@NonNull View widget) { listener.onViewTag(tag); } |
||||
}; |
||||
} else if (text.charAt(0) == '@' && mentions != null && mentions.size() > 0) { |
||||
// https://github.com/tuskyapp/Tusky/pull/2339
|
||||
String id = null; |
||||
for (Status.Mention mention : mentions) { |
||||
if (mention.getUrl().equals(span.getURL())) { |
||||
id = mention.getId(); |
||||
break; |
||||
} |
||||
} |
||||
if (id != null) { |
||||
final String accountId = id; |
||||
customSpan = new NoUnderlineURLSpan(span.getURL()) { |
||||
@Override |
||||
public void onClick(@NonNull View widget) { listener.onViewAccount(accountId); } |
||||
}; |
||||
} |
||||
} |
||||
|
||||
if (customSpan == null) { |
||||
customSpan = new NoUnderlineURLSpan(span.getURL()) { |
||||
@Override |
||||
public void onClick(@NonNull View widget) { |
||||
listener.onViewUrl(getURL()); |
||||
} |
||||
}; |
||||
} |
||||
builder.removeSpan(span); |
||||
builder.setSpan(customSpan, start, end, flags); |
||||
|
||||
/* Add zero-width space after links in end of line to fix its too large hitbox. |
||||
* See also : https://github.com/tuskyapp/Tusky/issues/846
|
||||
* https://github.com/tuskyapp/Tusky/pull/916 */
|
||||
if (end >= builder.length() || |
||||
builder.subSequence(end, end + 1).toString().equals("\n")){ |
||||
builder.insert(end, "\u200B"); |
||||
} |
||||
} |
||||
|
||||
view.setText(builder); |
||||
view.setMovementMethod(LinkMovementMethod.getInstance()); |
||||
} |
||||
|
||||
/** |
||||
* Put mentions in a piece of text and makes them clickable, associating them with callbacks to |
||||
* notify when they're clicked. |
||||
* |
||||
* @param view the returned text will be put in |
||||
* @param mentions any '@' mentions which are known to be in the content |
||||
* @param listener to notify about particular spans that are clicked |
||||
*/ |
||||
public static void setClickableMentions( |
||||
TextView view, @Nullable List<Status.Mention> mentions, final LinkListener listener) { |
||||
if (mentions == null || mentions.size() == 0) { |
||||
view.setText(null); |
||||
return; |
||||
} |
||||
SpannableStringBuilder builder = new SpannableStringBuilder(); |
||||
int start = 0; |
||||
int end = 0; |
||||
int flags; |
||||
boolean firstMention = true; |
||||
for (Status.Mention mention : mentions) { |
||||
String accountUsername = mention.getLocalUsername(); |
||||
final String accountId = mention.getId(); |
||||
ClickableSpan customSpan = new NoUnderlineURLSpan(mention.getUrl()) { |
||||
@Override |
||||
public void onClick(@NonNull View widget) { listener.onViewAccount(accountId); } |
||||
}; |
||||
|
||||
end += 1 + accountUsername.length(); // length of @ + username
|
||||
flags = builder.getSpanFlags(customSpan); |
||||
if (firstMention) { |
||||
firstMention = false; |
||||
} else { |
||||
builder.append(" "); |
||||
start += 1; |
||||
end += 1; |
||||
} |
||||
builder.append("@"); |
||||
builder.append(accountUsername); |
||||
builder.setSpan(customSpan, start, end, flags); |
||||
builder.append("\u200B"); // same reasonning than in setClickableText
|
||||
end += 1; // shift position to take the previous character into account
|
||||
start = end; |
||||
} |
||||
view.setText(builder); |
||||
view.setMovementMethod(LinkMovementMethod.getInstance()); |
||||
} |
||||
|
||||
public static CharSequence createClickableText(String text, String link) { |
||||
URLSpan span = new NoUnderlineURLSpan(link); |
||||
|
||||
SpannableStringBuilder clickableText = new SpannableStringBuilder(text); |
||||
clickableText.setSpan(span, 0, text.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); |
||||
return clickableText; |
||||
} |
||||
|
||||
/** |
||||
* Opens a link, depending on the settings, either in the browser or in a custom tab |
||||
* |
||||
* @param url a string containing the url to open |
||||
* @param context context |
||||
*/ |
||||
public static void openLink(String url, Context context) { |
||||
Uri uri = Uri.parse(url).normalizeScheme(); |
||||
|
||||
boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(context) |
||||
.getBoolean("customTabs", false); |
||||
if (useCustomTabs) { |
||||
openLinkInCustomTab(uri, context); |
||||
} else { |
||||
openLinkInBrowser(uri, context); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* opens a link in the browser via Intent.ACTION_VIEW |
||||
* |
||||
* @param uri the uri to open |
||||
* @param context context |
||||
*/ |
||||
public static void openLinkInBrowser(Uri uri, Context context) { |
||||
Intent intent = new Intent(Intent.ACTION_VIEW, uri); |
||||
try { |
||||
context.startActivity(intent); |
||||
} catch (ActivityNotFoundException e) { |
||||
Log.w("LinkHelper", "Actvity was not found for intent, " + intent); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* tries to open a link in a custom tab |
||||
* falls back to browser if not possible |
||||
* |
||||
* @param uri the uri to open |
||||
* @param context context |
||||
*/ |
||||
public static void openLinkInCustomTab(Uri uri, Context context) { |
||||
int toolbarColor = ThemeUtils.getColor(context, R.attr.colorSurface); |
||||
int navigationbarColor = ThemeUtils.getColor(context, android.R.attr.navigationBarColor); |
||||
int navigationbarDividerColor = ThemeUtils.getColor(context, R.attr.dividerColor); |
||||
|
||||
CustomTabColorSchemeParams colorSchemeParams = new CustomTabColorSchemeParams.Builder() |
||||
.setToolbarColor(toolbarColor) |
||||
.setNavigationBarColor(navigationbarColor) |
||||
.setNavigationBarDividerColor(navigationbarDividerColor) |
||||
.build(); |
||||
|
||||
CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder() |
||||
.setDefaultColorSchemeParams(colorSchemeParams) |
||||
.setShowTitle(true) |
||||
.build(); |
||||
|
||||
try { |
||||
customTabsIntent.launchUrl(context, uri); |
||||
} catch (ActivityNotFoundException e) { |
||||
Log.w("LinkHelper", "Activity was not found for intent " + customTabsIntent); |
||||
openLinkInBrowser(uri, context); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,239 @@
|
||||
/* 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>. */ |
||||
@file:JvmName("LinkHelper") |
||||
|
||||
package com.keylesspalace.tusky.util |
||||
|
||||
import android.content.ActivityNotFoundException |
||||
import android.content.Context |
||||
import android.content.Intent |
||||
import android.net.Uri |
||||
import android.text.SpannableStringBuilder |
||||
import android.text.Spanned |
||||
import android.text.method.LinkMovementMethod |
||||
import android.text.style.ClickableSpan |
||||
import android.text.style.URLSpan |
||||
import android.util.Log |
||||
import android.view.View |
||||
import android.widget.TextView |
||||
import androidx.annotation.VisibleForTesting |
||||
import androidx.browser.customtabs.CustomTabColorSchemeParams |
||||
import androidx.browser.customtabs.CustomTabsIntent |
||||
import androidx.core.net.toUri |
||||
import androidx.preference.PreferenceManager |
||||
import com.keylesspalace.tusky.R |
||||
import com.keylesspalace.tusky.entity.HashTag |
||||
import com.keylesspalace.tusky.entity.Status.Mention |
||||
import com.keylesspalace.tusky.interfaces.LinkListener |
||||
|
||||
fun getDomain(urlString: String?): String { |
||||
val host = urlString?.toUri()?.host |
||||
return when { |
||||
host == null -> "" |
||||
host.startsWith("www.") -> host.substring(4) |
||||
else -> host |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Finds links, mentions, and hashtags in a piece of text and makes them clickable, associating |
||||
* them with callbacks to notify when they're clicked. |
||||
* |
||||
* @param view the returned text will be put in |
||||
* @param content containing text with mentions, links, or hashtags |
||||
* @param mentions any '@' mentions which are known to be in the content |
||||
* @param listener to notify about particular spans that are clicked |
||||
*/ |
||||
fun setClickableText(view: TextView, content: CharSequence, mentions: List<Mention>, tags: List<HashTag>?, listener: LinkListener) { |
||||
view.text = SpannableStringBuilder.valueOf(content).apply { |
||||
getSpans(0, content.length, URLSpan::class.java).forEach { |
||||
setClickableText(it, this, mentions, tags, listener) |
||||
} |
||||
} |
||||
view.movementMethod = LinkMovementMethod.getInstance() |
||||
} |
||||
|
||||
@VisibleForTesting |
||||
fun setClickableText( |
||||
span: URLSpan, |
||||
builder: SpannableStringBuilder, |
||||
mentions: List<Mention>, |
||||
tags: List<HashTag>?, |
||||
listener: LinkListener |
||||
) = builder.apply { |
||||
val start = getSpanStart(span) |
||||
val end = getSpanEnd(span) |
||||
val flags = getSpanFlags(span) |
||||
val text = subSequence(start, end) |
||||
|
||||
val customSpan = when (text[0]) { |
||||
'#' -> getCustomSpanForTag(text, tags, span, listener) |
||||
'@' -> getCustomSpanForMention(mentions, span, listener) |
||||
else -> null |
||||
} ?: object : NoUnderlineURLSpan(span.url) { |
||||
override fun onClick(view: View) = listener.onViewUrl(url) |
||||
} |
||||
|
||||
removeSpan(span) |
||||
setSpan(customSpan, start, end, flags) |
||||
|
||||
/* Add zero-width space after links in end of line to fix its too large hitbox. |
||||
* See also : https://github.com/tuskyapp/Tusky/issues/846 |
||||
* https://github.com/tuskyapp/Tusky/pull/916 */ |
||||
if (end >= length || subSequence(end, end + 1).toString() == "\n") { |
||||
insert(end, "\u200B") |
||||
} |
||||
} |
||||
|
||||
@VisibleForTesting |
||||
fun getTagName(text: CharSequence, tags: List<HashTag>?, span: URLSpan): String? { |
||||
return when (tags) { |
||||
null -> text.subSequence(1, text.length).toString() |
||||
else -> tags.firstOrNull { it.url == span.url }?.name |
||||
} |
||||
} |
||||
|
||||
private fun getCustomSpanForTag(text: CharSequence, tags: List<HashTag>?, span: URLSpan, listener: LinkListener): ClickableSpan? { |
||||
return getTagName(text, tags, span)?.let { |
||||
object : NoUnderlineURLSpan(span.url) { |
||||
override fun onClick(view: View) = listener.onViewTag(it) |
||||
} |
||||
} |
||||
} |
||||
|
||||
private fun getCustomSpanForMention(mentions: List<Mention>, span: URLSpan, listener: LinkListener): ClickableSpan? { |
||||
// https://github.com/tuskyapp/Tusky/pull/2339 |
||||
return mentions.firstOrNull { it.url == span.url }?.let { |
||||
getCustomSpanForMentionUrl(span.url, it.id, listener) |
||||
} |
||||
} |
||||
|
||||
private fun getCustomSpanForMentionUrl(url: String, mentionId: String, listener: LinkListener): ClickableSpan { |
||||
return object : NoUnderlineURLSpan(url) { |
||||
override fun onClick(view: View) = listener.onViewAccount(mentionId) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Put mentions in a piece of text and makes them clickable, associating them with callbacks to |
||||
* notify when they're clicked. |
||||
* |
||||
* @param view the returned text will be put in |
||||
* @param mentions any '@' mentions which are known to be in the content |
||||
* @param listener to notify about particular spans that are clicked |
||||
*/ |
||||
fun setClickableMentions(view: TextView, mentions: List<Mention>?, listener: LinkListener) { |
||||
if (mentions?.isEmpty() != false) { |
||||
view.text = null |
||||
return |
||||
} |
||||
|
||||
view.text = SpannableStringBuilder().apply { |
||||
var start = 0 |
||||
var end = 0 |
||||
var flags: Int |
||||
var firstMention = true |
||||
|
||||
for (mention in mentions) { |
||||
val customSpan = getCustomSpanForMentionUrl(mention.url, mention.id, listener) |
||||
end += 1 + mention.username.length // length of @ + username |
||||
flags = getSpanFlags(customSpan) |
||||
if (firstMention) { |
||||
firstMention = false |
||||
} else { |
||||
append(" ") |
||||
start += 1 |
||||
end += 1 |
||||
} |
||||
|
||||
append("@") |
||||
append(mention.username) |
||||
setSpan(customSpan, start, end, flags) |
||||
append("\u200B") // same reasoning as in setClickableText |
||||
end += 1 // shift position to take the previous character into account |
||||
start = end |
||||
} |
||||
} |
||||
view.movementMethod = LinkMovementMethod.getInstance() |
||||
} |
||||
|
||||
fun createClickableText(text: String, link: String): CharSequence { |
||||
return SpannableStringBuilder(text).apply { |
||||
setSpan(NoUnderlineURLSpan(link), 0, text.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Opens a link, depending on the settings, either in the browser or in a custom tab |
||||
* |
||||
* @receiver the Context to open the link from |
||||
* @param url a string containing the url to open |
||||
*/ |
||||
fun Context.openLink(url: String) { |
||||
val uri = url.toUri().normalizeScheme() |
||||
val useCustomTabs = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("customTabs", false) |
||||
|
||||
if (useCustomTabs) { |
||||
openLinkInCustomTab(uri, this) |
||||
} else { |
||||
openLinkInBrowser(uri, this) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* opens a link in the browser via Intent.ACTION_VIEW |
||||
* |
||||
* @param uri the uri to open |
||||
* @param context context |
||||
*/ |
||||
private fun openLinkInBrowser(uri: Uri?, context: Context) { |
||||
val intent = Intent(Intent.ACTION_VIEW, uri) |
||||
try { |
||||
context.startActivity(intent) |
||||
} catch (e: ActivityNotFoundException) { |
||||
Log.w(TAG, "Actvity was not found for intent, $intent") |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* tries to open a link in a custom tab |
||||
* falls back to browser if not possible |
||||
* |
||||
* @param uri the uri to open |
||||
* @param context context |
||||
*/ |
||||
private fun openLinkInCustomTab(uri: Uri, context: Context) { |
||||
val toolbarColor = ThemeUtils.getColor(context, R.attr.colorSurface) |
||||
val navigationbarColor = ThemeUtils.getColor(context, android.R.attr.navigationBarColor) |
||||
val navigationbarDividerColor = ThemeUtils.getColor(context, R.attr.dividerColor) |
||||
val colorSchemeParams = CustomTabColorSchemeParams.Builder() |
||||
.setToolbarColor(toolbarColor) |
||||
.setNavigationBarColor(navigationbarColor) |
||||
.setNavigationBarDividerColor(navigationbarDividerColor) |
||||
.build() |
||||
val customTabsIntent = CustomTabsIntent.Builder() |
||||
.setDefaultColorSchemeParams(colorSchemeParams) |
||||
.setShowTitle(true) |
||||
.build() |
||||
|
||||
try { |
||||
customTabsIntent.launchUrl(context, uri) |
||||
} catch (e: ActivityNotFoundException) { |
||||
Log.w(TAG, "Activity was not found for intent $customTabsIntent") |
||||
openLinkInBrowser(uri, context) |
||||
} |
||||
} |
||||
|
||||
private const val TAG = "LinkHelper" |
||||
@ -0,0 +1,172 @@
|
||||
package com.keylesspalace.tusky.util |
||||
|
||||
import android.text.SpannableStringBuilder |
||||
import android.text.style.URLSpan |
||||
import androidx.test.ext.junit.runners.AndroidJUnit4 |
||||
import com.keylesspalace.tusky.entity.HashTag |
||||
import com.keylesspalace.tusky.entity.Status |
||||
import com.keylesspalace.tusky.interfaces.LinkListener |
||||
import org.junit.Assert |
||||
import org.junit.Test |
||||
import org.junit.runner.RunWith |
||||
import org.robolectric.annotation.Config |
||||
|
||||
@Config(sdk = [28]) |
||||
@RunWith(AndroidJUnit4::class) |
||||
class LinkHelperTest { |
||||
private val listener = object : LinkListener { |
||||
override fun onViewTag(tag: String?) { } |
||||
override fun onViewAccount(id: String?) { } |
||||
override fun onViewUrl(url: String?) { } |
||||
} |
||||
|
||||
private val mentions = listOf( |
||||
Status.Mention("1", "https://example.com/@user", "user", "user"), |
||||
Status.Mention("2", "https://example.com/@anotherUser", "anotherUser", "anotherUser"), |
||||
) |
||||
private val tags = listOf( |
||||
HashTag("Tusky", "https://example.com/Tags/Tusky"), |
||||
HashTag("mastodev", "https://example.com/Tags/mastodev"), |
||||
) |
||||
|
||||
@Test |
||||
fun whenSettingClickableText_mentionUrlsArePreserved() { |
||||
val builder = SpannableStringBuilder() |
||||
for (mention in mentions) { |
||||
builder.append("@${mention.username}", URLSpan(mention.url), 0) |
||||
builder.append(" ") |
||||
} |
||||
|
||||
var urlSpans = builder.getSpans(0, builder.length, URLSpan::class.java) |
||||
for (span in urlSpans) { |
||||
setClickableText(span, builder, mentions, null, listener) |
||||
} |
||||
|
||||
urlSpans = builder.getSpans(0, builder.length, URLSpan::class.java) |
||||
for (span in urlSpans) { |
||||
Assert.assertNotNull(mentions.firstOrNull { it.url == span.url }) |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun whenSettingClickableText_nonMentionsAreNotConvertedToMentions() { |
||||
val builder = SpannableStringBuilder() |
||||
val nonMentionUrl = "http://example.com/" |
||||
for (mention in mentions) { |
||||
builder.append("@${mention.username}", URLSpan(nonMentionUrl), 0) |
||||
builder.append(" ") |
||||
builder.append("@${mention.username} ") |
||||
} |
||||
|
||||
var urlSpans = builder.getSpans(0, builder.length, URLSpan::class.java) |
||||
for (span in urlSpans) { |
||||
setClickableText(span, builder, mentions, null, listener) |
||||
} |
||||
|
||||
urlSpans = builder.getSpans(0, builder.length, URLSpan::class.java) |
||||
for (span in urlSpans) { |
||||
Assert.assertEquals(nonMentionUrl, span.url) |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun whenSettingClickableTest_tagUrlsArePreserved() { |
||||
val builder = SpannableStringBuilder() |
||||
for (tag in tags) { |
||||
builder.append("#${tag.name}", URLSpan(tag.url), 0) |
||||
builder.append(" ") |
||||
} |
||||
|
||||
var urlSpans = builder.getSpans(0, builder.length, URLSpan::class.java) |
||||
for (span in urlSpans) { |
||||
setClickableText(span, builder, emptyList(), tags, listener) |
||||
} |
||||
|
||||
urlSpans = builder.getSpans(0, builder.length, URLSpan::class.java) |
||||
for (span in urlSpans) { |
||||
Assert.assertNotNull(tags.firstOrNull { it.url == span.url }) |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun whenSettingClickableTest_nonTagUrlsAreNotConverted() { |
||||
val builder = SpannableStringBuilder() |
||||
val nonTagUrl = "http://example.com/" |
||||
for (tag in tags) { |
||||
builder.append("#${tag.name}", URLSpan(nonTagUrl), 0) |
||||
builder.append(" ") |
||||
builder.append("#${tag.name} ") |
||||
} |
||||
|
||||
var urlSpans = builder.getSpans(0, builder.length, URLSpan::class.java) |
||||
for (span in urlSpans) { |
||||
setClickableText(span, builder, emptyList(), tags, listener) |
||||
} |
||||
|
||||
urlSpans = builder.getSpans(0, builder.length, URLSpan::class.java) |
||||
for (span in urlSpans) { |
||||
Assert.assertEquals(nonTagUrl, span.url) |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun whenTagsAreNull_tagNameIsGeneratedFromText() { |
||||
SpannableStringBuilder().apply { |
||||
for (tag in tags) { |
||||
append("#${tag.name}", URLSpan(tag.url), 0) |
||||
append(" ") |
||||
} |
||||
|
||||
getSpans(0, length, URLSpan::class.java).forEach { |
||||
Assert.assertNotNull(getTagName(subSequence(getSpanStart(it), getSpanEnd(it)), null, it)) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun whenStringIsInvalidUri_emptyStringIsReturnedFromGetDomain() { |
||||
listOf( |
||||
null, |
||||
"foo bar baz", |
||||
"http:/foo.bar", |
||||
"c:/foo/bar", |
||||
).forEach { |
||||
Assert.assertEquals("", getDomain(it)) |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun whenUrlIsValid_correctDomainIsReturned() { |
||||
listOf( |
||||
"example.com", |
||||
"localhost", |
||||
"sub.domain.com", |
||||
"10.45.0.123", |
||||
).forEach { domain -> |
||||
listOf( |
||||
"https://$domain", |
||||
"https://$domain/", |
||||
"https://$domain/foo/bar", |
||||
"https://$domain/foo/bar.html", |
||||
"https://$domain/foo/bar.html#", |
||||
"https://$domain/foo/bar.html#anchor", |
||||
"https://$domain/foo/bar.html?argument=value", |
||||
"https://$domain/foo/bar.html?argument=value&otherArgument=otherValue", |
||||
).forEach { url -> |
||||
Assert.assertEquals(domain, getDomain(url)) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun wwwPrefixIsStrippedFromGetDomain() { |
||||
mapOf( |
||||
"https://www.example.com/foo/bar" to "example.com", |
||||
"https://awww.example.com/foo/bar" to "awww.example.com", |
||||
"http://www.localhost" to "localhost", |
||||
"https://wwwexample.com/" to "wwwexample.com", |
||||
).forEach { (url, domain) -> |
||||
Assert.assertEquals(domain, getDomain(url)) |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue