diff --git a/mastodon/src/main/java/com/twitter/twittertext/Regex.java b/mastodon/src/main/java/com/twitter/twittertext/Regex.java new file mode 100644 index 00000000..61a0fc9d --- /dev/null +++ b/mastodon/src/main/java/com/twitter/twittertext/Regex.java @@ -0,0 +1,349 @@ +// Copyright 2018 Twitter, Inc. +// Licensed under the Apache License, Version 2.0 +// http://www.apache.org/licenses/LICENSE-2.0 + +package com.twitter.twittertext; + +import java.util.Collection; +import java.util.Iterator; +import java.util.regex.Pattern; + +import androidx.annotation.NonNull; + +public class Regex { + + protected Regex() { + } + + private static final String URL_VALID_GTLD = + "(?:(?:" + + join(TldLists.GTLDS) + + ")(?=[^a-z0-9@+-]|$))"; + private static final String URL_VALID_CCTLD = + "(?:(?:" + + join(TldLists.CTLDS) + + ")(?=[^a-z0-9@+-]|$))"; + + private static final String INVALID_CHARACTERS = + "\\uFFFE" + // BOM + "\\uFEFF" + // BOM + "\\uFFFF"; // Special + + private static final String DIRECTIONAL_CHARACTERS = + "\\u061C" + // ARABIC LETTER MARK (ALM) + "\\u200E" + // LEFT-TO-RIGHT MARK (LRM) + "\\u200F" + // RIGHT-TO-LEFT MARK (RLM) + "\\u202A" + // LEFT-TO-RIGHT EMBEDDING (LRE) + "\\u202B" + // RIGHT-TO-LEFT EMBEDDING (RLE) + "\\u202C" + // POP DIRECTIONAL FORMATTING (PDF) + "\\u202D" + // LEFT-TO-RIGHT OVERRIDE (LRO) + "\\u202E" + // RIGHT-TO-LEFT OVERRIDE (RLO) + "\\u2066" + // LEFT-TO-RIGHT ISOLATE (LRI) + "\\u2067" + // RIGHT-TO-LEFT ISOLATE (RLI) + "\\u2068" + // FIRST STRONG ISOLATE (FSI) + "\\u2069"; // POP DIRECTIONAL ISOLATE (PDI) + + + private static final String UNICODE_SPACES = "[" + + "\\u0009-\\u000d" + // # White_Space # Cc [5] .. + "\\u0020" + // White_Space # Zs SPACE + "\\u0085" + // White_Space # Cc + "\\u00a0" + // White_Space # Zs NO-BREAK SPACE + "\\u1680" + // White_Space # Zs OGHAM SPACE MARK + "\\u180E" + // White_Space # Zs MONGOLIAN VOWEL SEPARATOR + "\\u2000-\\u200a" + // # White_Space # Zs [11] EN QUAD..HAIR SPACE + "\\u2028" + // White_Space # Zl LINE SEPARATOR + "\\u2029" + // White_Space # Zp PARAGRAPH SEPARATOR + "\\u202F" + // White_Space # Zs NARROW NO-BREAK SPACE + "\\u205F" + // White_Space # Zs MEDIUM MATHEMATICAL SPACE + "\\u3000" + // White_Space # Zs IDEOGRAPHIC SPACE + "]"; + + private static final String LATIN_ACCENTS_CHARS = + // Latin-1 + "\\u00c0-\\u00d6\\u00d8-\\u00f6\\u00f8-\\u00ff" + + // Latin Extended A and B + "\\u0100-\\u024f" + + // IPA Extensions + "\\u0253\\u0254\\u0256\\u0257\\u0259\\u025b\\u0263\\u0268\\u026f\\u0272\\u0289\\u028b" + + // Hawaiian + "\\u02bb" + + // Combining diacritics + "\\u0300-\\u036f" + + // Latin Extended Additional (mostly for Vietnamese) + "\\u1e00-\\u1eff"; + + private static final String CYRILLIC_CHARS = "\\u0400-\\u04ff"; + + // Generated from unicode_regex/unicode_regex_groups.scala, more inclusive than Java's \p{L}\p{M} + private static final String HASHTAG_LETTERS_AND_MARKS = "\\p{L}\\p{M}" + + "\\u037f\\u0528-\\u052f\\u08a0-\\u08b2\\u08e4-\\u08ff\\u0978\\u0980\\u0c00\\u0c34\\u0c81" + + "\\u0d01\\u0ede\\u0edf\\u10c7\\u10cd\\u10fd-\\u10ff\\u16f1-\\u16f8\\u17b4\\u17b5\\u191d" + + "\\u191e\\u1ab0-\\u1abe\\u1bab-\\u1bad\\u1bba-\\u1bbf\\u1cf3-\\u1cf6\\u1cf8\\u1cf9" + + "\\u1de7-\\u1df5\\u2cf2\\u2cf3\\u2d27\\u2d2d\\u2d66\\u2d67\\u9fcc\\ua674-\\ua67b\\ua698" + + "-\\ua69d\\ua69f\\ua792-\\ua79f\\ua7aa-\\ua7ad\\ua7b0\\ua7b1\\ua7f7-\\ua7f9\\ua9e0-" + + "\\ua9ef\\ua9fa-\\ua9fe\\uaa7c-\\uaa7f\\uaae0-\\uaaef\\uaaf2-\\uaaf6\\uab30-\\uab5a" + + "\\uab5c-\\uab5f\\uab64\\uab65\\uf870-\\uf87f\\uf882\\uf884-\\uf89f\\uf8b8\\uf8c1-" + + "\\uf8d6\\ufa2e\\ufa2f\\ufe27-\\ufe2d\\ud800\\udee0\\ud800\\udf1f\\ud800\\udf50-\\ud800" + + "\\udf7a\\ud801\\udd00-\\ud801\\udd27\\ud801\\udd30-\\ud801\\udd63\\ud801\\ude00-\\ud801" + + "\\udf36\\ud801\\udf40-\\ud801\\udf55\\ud801\\udf60-\\ud801\\udf67\\ud802\\udc60-\\ud802" + + "\\udc76\\ud802\\udc80-\\ud802\\udc9e\\ud802\\udd80-\\ud802\\uddb7\\ud802\\uddbe\\ud802" + + "\\uddbf\\ud802\\ude80-\\ud802\\ude9c\\ud802\\udec0-\\ud802\\udec7\\ud802\\udec9-\\ud802" + + "\\udee6\\ud802\\udf80-\\ud802\\udf91\\ud804\\udc7f\\ud804\\udcd0-\\ud804\\udce8\\ud804" + + "\\udd00-\\ud804\\udd34\\ud804\\udd50-\\ud804\\udd73\\ud804\\udd76\\ud804\\udd80-\\ud804" + + "\\uddc4\\ud804\\uddda\\ud804\\ude00-\\ud804\\ude11\\ud804\\ude13-\\ud804\\ude37\\ud804" + + "\\udeb0-\\ud804\\udeea\\ud804\\udf01-\\ud804\\udf03\\ud804\\udf05-\\ud804\\udf0c\\ud804" + + "\\udf0f\\ud804\\udf10\\ud804\\udf13-\\ud804\\udf28\\ud804\\udf2a-\\ud804\\udf30\\ud804" + + "\\udf32\\ud804\\udf33\\ud804\\udf35-\\ud804\\udf39\\ud804\\udf3c-\\ud804\\udf44\\ud804" + + "\\udf47\\ud804\\udf48\\ud804\\udf4b-\\ud804\\udf4d\\ud804\\udf57\\ud804\\udf5d-\\ud804" + + "\\udf63\\ud804\\udf66-\\ud804\\udf6c\\ud804\\udf70-\\ud804\\udf74\\ud805\\udc80-\\ud805" + + "\\udcc5\\ud805\\udcc7\\ud805\\udd80-\\ud805\\uddb5\\ud805\\uddb8-\\ud805\\uddc0\\ud805" + + "\\ude00-\\ud805\\ude40\\ud805\\ude44\\ud805\\ude80-\\ud805\\udeb7\\ud806\\udca0-\\ud806" + + "\\udcdf\\ud806\\udcff\\ud806\\udec0-\\ud806\\udef8\\ud808\\udf6f-\\ud808\\udf98\\ud81a" + + "\\ude40-\\ud81a\\ude5e\\ud81a\\uded0-\\ud81a\\udeed\\ud81a\\udef0-\\ud81a\\udef4\\ud81a" + + "\\udf00-\\ud81a\\udf36\\ud81a\\udf40-\\ud81a\\udf43\\ud81a\\udf63-\\ud81a\\udf77\\ud81a" + + "\\udf7d-\\ud81a\\udf8f\\ud81b\\udf00-\\ud81b\\udf44\\ud81b\\udf50-\\ud81b\\udf7e\\ud81b" + + "\\udf8f-\\ud81b\\udf9f\\ud82f\\udc00-\\ud82f\\udc6a\\ud82f\\udc70-\\ud82f\\udc7c\\ud82f" + + "\\udc80-\\ud82f\\udc88\\ud82f\\udc90-\\ud82f\\udc99\\ud82f\\udc9d\\ud82f\\udc9e\\ud83a" + + "\\udc00-\\ud83a\\udcc4\\ud83a\\udcd0-\\ud83a\\udcd6\\ud83b\\ude00-\\ud83b\\ude03\\ud83b" + + "\\ude05-\\ud83b\\ude1f\\ud83b\\ude21\\ud83b\\ude22\\ud83b\\ude24\\ud83b\\ude27\\ud83b" + + "\\ude29-\\ud83b\\ude32\\ud83b\\ude34-\\ud83b\\ude37\\ud83b\\ude39\\ud83b\\ude3b\\ud83b" + + "\\ude42\\ud83b\\ude47\\ud83b\\ude49\\ud83b\\ude4b\\ud83b\\ude4d-\\ud83b\\ude4f\\ud83b" + + "\\ude51\\ud83b\\ude52\\ud83b\\ude54\\ud83b\\ude57\\ud83b\\ude59\\ud83b\\ude5b\\ud83b" + + "\\ude5d\\ud83b\\ude5f\\ud83b\\ude61\\ud83b\\ude62\\ud83b\\ude64\\ud83b\\ude67-\\ud83b" + + "\\ude6a\\ud83b\\ude6c-\\ud83b\\ude72\\ud83b\\ude74-\\ud83b\\ude77\\ud83b\\ude79-\\ud83b" + + "\\ude7c\\ud83b\\ude7e\\ud83b\\ude80-\\ud83b\\ude89\\ud83b\\ude8b-\\ud83b\\ude9b\\ud83b" + + "\\udea1-\\ud83b\\udea3\\ud83b\\udea5-\\ud83b\\udea9\\ud83b\\udeab-\\ud83b\\udebb"; + + // Generated from unicode_regex/unicode_regex_groups.scala, more inclusive than Java's \p{Nd} + private static final String HASHTAG_NUMERALS = "\\p{Nd}" + + "\\u0de6-\\u0def\\ua9f0-\\ua9f9\\ud804\\udcf0-\\ud804\\udcf9\\ud804\\udd36-\\ud804" + + "\\udd3f\\ud804\\uddd0-\\ud804\\uddd9\\ud804\\udef0-\\ud804\\udef9\\ud805\\udcd0-\\ud805" + + "\\udcd9\\ud805\\ude50-\\ud805\\ude59\\ud805\\udec0-\\ud805\\udec9\\ud806\\udce0-\\ud806" + + "\\udce9\\ud81a\\ude60-\\ud81a\\ude69\\ud81a\\udf50-\\ud81a\\udf59"; + + private static final String HASHTAG_SPECIAL_CHARS = "_" + //underscore + "\\u200c" + // ZERO WIDTH NON-JOINER (ZWNJ) + "\\u200d" + // ZERO WIDTH JOINER (ZWJ) + "\\ua67e" + // CYRILLIC KAVYKA + "\\u05be" + // HEBREW PUNCTUATION MAQAF + "\\u05f3" + // HEBREW PUNCTUATION GERESH + "\\u05f4" + // HEBREW PUNCTUATION GERSHAYIM + "\\uff5e" + // FULLWIDTH TILDE + "\\u301c" + // WAVE DASH + "\\u309b" + // KATAKANA-HIRAGANA VOICED SOUND MARK + "\\u309c" + // KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK + "\\u30a0" + // KATAKANA-HIRAGANA DOUBLE HYPHEN + "\\u30fb" + // KATAKANA MIDDLE DOT + "\\u3003" + // DITTO MARK + "\\u0f0b" + // TIBETAN MARK INTERSYLLABIC TSHEG + "\\u0f0c" + // TIBETAN MARK DELIMITER TSHEG BSTAR + "\\u00b7"; // MIDDLE DOT + + private static final String HASHTAG_LETTERS_NUMERALS = + HASHTAG_LETTERS_AND_MARKS + HASHTAG_NUMERALS + HASHTAG_SPECIAL_CHARS; + private static final String HASHTAG_LETTERS_SET = "[" + HASHTAG_LETTERS_AND_MARKS + "]"; + private static final String HASHTAG_LETTERS_NUMERALS_SET = "[" + HASHTAG_LETTERS_NUMERALS + "]"; + + /* URL related hash regex collection */ + public static final String URL_VALID_PRECEDING_CHARS = + "(?:[^a-z0-9@@$##" + INVALID_CHARACTERS + "]|[" + DIRECTIONAL_CHARACTERS + "]|^)"; + + private static final String URL_VALID_CHARS = "[a-z0-9" + LATIN_ACCENTS_CHARS + "]"; + private static final String URL_VALID_SUBDOMAIN = + "(?>(?:" + URL_VALID_CHARS + "[" + URL_VALID_CHARS + "\\-_]*)?" + URL_VALID_CHARS + "\\.)"; + private static final String URL_VALID_DOMAIN_NAME = + "(?:(?:" + URL_VALID_CHARS + "[" + URL_VALID_CHARS + "\\-]*)?" + URL_VALID_CHARS + "\\.)"; + + private static final String PUNCTUATION_CHARS = "-_!\"#$%&'\\(\\)*+,./:;<=>?@\\[\\]^`\\{|}~"; + + // Any non-space, non-punctuation characters. + // \p{Z} = any kind of whitespace or invisible separator. + private static final String URL_VALID_UNICODE_CHARS = + "[^" + PUNCTUATION_CHARS + "\\s\\p{Z}\\p{InGeneralPunctuation}]"; + private static final String URL_VALID_UNICODE_DOMAIN_NAME = + "(?:(?:" + URL_VALID_UNICODE_CHARS + "[" + URL_VALID_UNICODE_CHARS + "\\-]*)?" + + URL_VALID_UNICODE_CHARS + "\\.)"; + + private static final String URL_PUNYCODE = "(?:xn--[-0-9a-z]+)"; + + public static final String URL_VALID_DOMAIN = + "(?:" + // optional sub-domain + domain + TLD + URL_VALID_SUBDOMAIN + "*" + URL_VALID_DOMAIN_NAME + // e.g. twitter.com, foo.co.jp ... + "(?:" + URL_VALID_GTLD + "|" + URL_VALID_CCTLD + "|" + URL_PUNYCODE + ")" + + ")" + + "|(?:" + "(?<=https?://)" + + "(?:" + + "(?:" + URL_VALID_DOMAIN_NAME + URL_VALID_CCTLD + ")" + // protocol + domain + ccTLD + "|(?:" + + URL_VALID_UNICODE_DOMAIN_NAME + // protocol + unicode domain + TLD + "(?:" + URL_VALID_GTLD + "|" + URL_VALID_CCTLD + ")" + + ")" + + ")" + + ")" + + "|(?:" + // domain + ccTLD + '/' + URL_VALID_DOMAIN_NAME + URL_VALID_CCTLD + "(?=/)" + // e.g. t.co/ + ")"; + + public static final String URL_VALID_PORT_NUMBER = "[0-9]++"; + + private static final String URL_VALID_GENERAL_PATH_CHARS = + "[a-z0-9!\\*';:=\\+,.\\$/%#\\[\\]\\-\\u2013_~\\|&@" + + LATIN_ACCENTS_CHARS + CYRILLIC_CHARS + "]"; + + /** + * Allow URL paths to contain up to two nested levels of balanced parens + * 1. Used in Wikipedia URLs like /Primer_(film) + * 2. Used in IIS sessions like /S(dfd346)/ + * 3. Used in Rdio URLs like /track/We_Up_(Album_Version_(Edited))/ + */ + private static final String URL_BALANCED_PARENS = "\\(" + + "(?:" + + URL_VALID_GENERAL_PATH_CHARS + "+" + + "|" + + // allow one nested level of balanced parentheses + "(?:" + + URL_VALID_GENERAL_PATH_CHARS + "*" + + "\\(" + + URL_VALID_GENERAL_PATH_CHARS + "+" + + "\\)" + + URL_VALID_GENERAL_PATH_CHARS + "*" + + ")" + + ")" + + "\\)"; + + /** + * Valid end-of-path characters (so /foo. does not gobble the period). + * 2. Allow =&# for empty URL parameters and other URL-join artifacts + */ + private static final String URL_VALID_PATH_ENDING_CHARS = + "[a-z0-9=_#/\\-\\+" + LATIN_ACCENTS_CHARS + CYRILLIC_CHARS + "]|(?:" + + URL_BALANCED_PARENS + ")"; + + public static final String URL_VALID_PATH = "(?:" + + "(?:" + + URL_VALID_GENERAL_PATH_CHARS + "*" + + "(?:" + URL_BALANCED_PARENS + URL_VALID_GENERAL_PATH_CHARS + "*)*" + + URL_VALID_PATH_ENDING_CHARS + + ")|(?:@" + URL_VALID_GENERAL_PATH_CHARS + "+/)" + + ")"; + + public static final String URL_VALID_URL_QUERY_CHARS = + "[a-z0-9!?\\*'\\(\\);:&=\\+\\$/%#\\[\\]\\-_\\.,~\\|@]"; + public static final String URL_VALID_URL_QUERY_ENDING_CHARS = "[a-z0-9\\-_&=#/]"; + private static final String VALID_URL_PATTERN_STRING = + "(" + // $1 total match + "(" + URL_VALID_PRECEDING_CHARS + ")" + // $2 Preceding character + "(" + // $3 URL + "(https?://)?" + // $4 Protocol (optional) + "(" + URL_VALID_DOMAIN + ")" + // $5 Domain(s) + "(?::(" + URL_VALID_PORT_NUMBER + "))?" + // $6 Port number (optional) + "(/" + + URL_VALID_PATH + "*+" + + ")?" + // $7 URL Path and anchor + "(\\?" + URL_VALID_URL_QUERY_CHARS + "*" + // $8 Query String + URL_VALID_URL_QUERY_ENDING_CHARS + ")?" + + ")" + + ")"; + + private static final String AT_SIGNS_CHARS = "@\uFF20"; + private static final String DOLLAR_SIGN_CHAR = "\\$"; + private static final String CASHTAG = "[a-z]{1,6}(?:[._][a-z]{1,2})?"; + + /* Begin public constants */ + + public static final Pattern INVALID_CHARACTERS_PATTERN; + public static final Pattern VALID_HASHTAG; + public static final int VALID_HASHTAG_GROUP_BEFORE = 1; + public static final int VALID_HASHTAG_GROUP_HASH = 2; + public static final int VALID_HASHTAG_GROUP_TAG = 3; + public static final Pattern INVALID_HASHTAG_MATCH_END; + public static final Pattern RTL_CHARACTERS; + + public static final Pattern AT_SIGNS; + public static final Pattern VALID_MENTION_OR_LIST; + public static final int VALID_MENTION_OR_LIST_GROUP_BEFORE = 1; + public static final int VALID_MENTION_OR_LIST_GROUP_AT = 2; + public static final int VALID_MENTION_OR_LIST_GROUP_USERNAME = 3; + public static final int VALID_MENTION_OR_LIST_GROUP_LIST = 4; + + public static final Pattern VALID_REPLY; + public static final int VALID_REPLY_GROUP_USERNAME = 1; + + public static final Pattern INVALID_MENTION_MATCH_END; + + /** + * Regex to extract URL (it also includes the text preceding the url). + * + * This regex does not reflect its name and {@link Regex#VALID_URL_GROUP_URL} match + * should be checked in order to match a valid url. This is not ideal, but the behavior is + * being kept to ensure backwards compatibility. Ideally this regex should be + * implemented with a negative lookbehind as opposed to a negated character class + * but lack of JS support increases maint overhead if the logic is different by + * platform. + */ + + public static final Pattern VALID_URL; + public static final int VALID_URL_GROUP_ALL = 1; + public static final int VALID_URL_GROUP_BEFORE = 2; + public static final int VALID_URL_GROUP_URL = 3; + public static final int VALID_URL_GROUP_PROTOCOL = 4; + public static final int VALID_URL_GROUP_DOMAIN = 5; + public static final int VALID_URL_GROUP_PORT = 6; + public static final int VALID_URL_GROUP_PATH = 7; + public static final int VALID_URL_GROUP_QUERY_STRING = 8; + + public static final Pattern VALID_TCO_URL; + public static final Pattern INVALID_URL_WITHOUT_PROTOCOL_MATCH_BEGIN; + + public static final Pattern VALID_CASHTAG; + public static final int VALID_CASHTAG_GROUP_BEFORE = 1; + public static final int VALID_CASHTAG_GROUP_DOLLAR = 2; + public static final int VALID_CASHTAG_GROUP_CASHTAG = 3; + + public static final Pattern VALID_DOMAIN; + + // initializing in a static synchronized block, + // there appears to be thread safety issues with Pattern.compile in android + static { + synchronized (Regex.class) { + INVALID_CHARACTERS_PATTERN = Pattern.compile(".*[" + INVALID_CHARACTERS + "].*"); + VALID_HASHTAG = Pattern.compile("(^|\\uFE0E|\\uFE0F|[^&" + HASHTAG_LETTERS_NUMERALS + + "])([#\uFF03])(?![\uFE0F\u20E3])(" + HASHTAG_LETTERS_NUMERALS_SET + "*" + + HASHTAG_LETTERS_SET + HASHTAG_LETTERS_NUMERALS_SET + "*)", Pattern.CASE_INSENSITIVE); + INVALID_HASHTAG_MATCH_END = Pattern.compile("^(?:[##]|://)"); + RTL_CHARACTERS = Pattern.compile("[\u0600-\u06FF\u0750-\u077F\u0590-\u05FF\uFE70-\uFEFF]"); + AT_SIGNS = Pattern.compile("[" + AT_SIGNS_CHARS + "]"); + VALID_MENTION_OR_LIST = Pattern.compile("([^a-z0-9_!#$%&*" + AT_SIGNS_CHARS + + "]|^|(?:^|[^a-z0-9_+~.-])RT:?)(" + AT_SIGNS + + "+)([a-z0-9_]{1,20})(/[a-z][a-z0-9_\\-]{0,24})?", Pattern.CASE_INSENSITIVE); + VALID_REPLY = Pattern.compile("^(?:" + UNICODE_SPACES + "|" + DIRECTIONAL_CHARACTERS + ")*" + + AT_SIGNS + "([a-z0-9_]{1,20})", Pattern.CASE_INSENSITIVE); + INVALID_MENTION_MATCH_END = + Pattern.compile("^(?:[" + AT_SIGNS_CHARS + LATIN_ACCENTS_CHARS + "]|://)"); + INVALID_URL_WITHOUT_PROTOCOL_MATCH_BEGIN = Pattern.compile("[-_./]$"); + + VALID_URL = Pattern.compile(VALID_URL_PATTERN_STRING, Pattern.CASE_INSENSITIVE); + VALID_TCO_URL = Pattern.compile("^https?://t\\.co/([a-z0-9]+)(?:\\?" + + URL_VALID_URL_QUERY_CHARS + "*" + URL_VALID_URL_QUERY_ENDING_CHARS + ")?", + Pattern.CASE_INSENSITIVE); + VALID_CASHTAG = Pattern.compile("(^|" + UNICODE_SPACES + "|" + DIRECTIONAL_CHARACTERS + ")(" + + DOLLAR_SIGN_CHAR + ")(" + CASHTAG + ")" + "(?=$|\\s|\\p{Punct})", + Pattern.CASE_INSENSITIVE); + VALID_DOMAIN = Pattern.compile(URL_VALID_DOMAIN, Pattern.CASE_INSENSITIVE); + } + } + + private static String join(@NonNull Collection col) { + final StringBuilder sb = new StringBuilder(); + final Iterator iter = col.iterator(); + if (iter.hasNext()) { + sb.append(iter.next().toString()); + } + while (iter.hasNext()) { + sb.append("|"); + sb.append(iter.next().toString()); + } + return sb.toString(); + } +} diff --git a/mastodon/src/main/java/com/twitter/twittertext/TldLists.java b/mastodon/src/main/java/com/twitter/twittertext/TldLists.java new file mode 100644 index 00000000..b54391b8 --- /dev/null +++ b/mastodon/src/main/java/com/twitter/twittertext/TldLists.java @@ -0,0 +1,1595 @@ +// Copyright 2018 Twitter, Inc. +// Licensed under the Apache License, Version 2.0 +// http://www.apache.org/licenses/LICENSE-2.0 + +// Auto-generated by conformance/Rakefile + +package com.twitter.twittertext; + +import java.util.Arrays; +import java.util.List; + +public final class TldLists { + private TldLists() { + } + + public static final List GTLDS = Arrays.asList( + "삼성", + "닷컴", + "닷넷", + "香格里拉", + "餐厅", + "食品", + "飞利浦", + "電訊盈科", + "集团", + "通販", + "购物", + "谷歌", + "诺基亚", + "联通", + "网络", + "网站", + "网店", + "网址", + "组织机构", + "移动", + "珠宝", + "点看", + "游戏", + "淡马锡", + "机构", + "書籍", + "时尚", + "新闻", + "政府", + "政务", + "招聘", + "手表", + "手机", + "我爱你", + "慈善", + "微博", + "广东", + "工行", + "家電", + "娱乐", + "天主教", + "大拿", + "大众汽车", + "在线", + "嘉里大酒店", + "嘉里", + "商标", + "商店", + "商城", + "公益", + "公司", + "八卦", + "健康", + "信息", + "佛山", + "企业", + "中文网", + "中信", + "世界", + "ポイント", + "ファッション", + "セール", + "ストア", + "コム", + "グーグル", + "クラウド", + "みんな", + "คอม", + "संगठन", + "नेट", + "कॉम", + "همراه", + "موقع", + "موبايلي", + "كوم", + "كاثوليك", + "عرب", + "شبكة", + "بيتك", + "بازار", + "العليان", + "ارامكو", + "اتصالات", + "ابوظبي", + "קום", + "сайт", + "рус", + "орг", + "онлайн", + "москва", + "ком", + "католик", + "дети", + "zuerich", + "zone", + "zippo", + "zip", + "zero", + "zara", + "zappos", + "yun", + "youtube", + "you", + "yokohama", + "yoga", + "yodobashi", + "yandex", + "yamaxun", + "yahoo", + "yachts", + "xyz", + "xxx", + "xperia", + "xin", + "xihuan", + "xfinity", + "xerox", + "xbox", + "wtf", + "wtc", + "wow", + "world", + "works", + "work", + "woodside", + "wolterskluwer", + "wme", + "winners", + "wine", + "windows", + "win", + "williamhill", + "wiki", + "wien", + "whoswho", + "weir", + "weibo", + "wedding", + "wed", + "website", + "weber", + "webcam", + "weatherchannel", + "weather", + "watches", + "watch", + "warman", + "wanggou", + "wang", + "walter", + "walmart", + "wales", + "vuelos", + "voyage", + "voto", + "voting", + "vote", + "volvo", + "volkswagen", + "vodka", + "vlaanderen", + "vivo", + "viva", + "vistaprint", + "vista", + "vision", + "visa", + "virgin", + "vip", + "vin", + "villas", + "viking", + "vig", + "video", + "viajes", + "vet", + "versicherung", + "vermögensberatung", + "vermögensberater", + "verisign", + "ventures", + "vegas", + "vanguard", + "vana", + "vacations", + "ups", + "uol", + "uno", + "university", + "unicom", + "uconnect", + "ubs", + "ubank", + "tvs", + "tushu", + "tunes", + "tui", + "tube", + "trv", + "trust", + "travelersinsurance", + "travelers", + "travelchannel", + "travel", + "training", + "trading", + "trade", + "toys", + "toyota", + "town", + "tours", + "total", + "toshiba", + "toray", + "top", + "tools", + "tokyo", + "today", + "tmall", + "tkmaxx", + "tjx", + "tjmaxx", + "tirol", + "tires", + "tips", + "tiffany", + "tienda", + "tickets", + "tiaa", + "theatre", + "theater", + "thd", + "teva", + "tennis", + "temasek", + "telefonica", + "telecity", + "tel", + "technology", + "tech", + "team", + "tdk", + "tci", + "taxi", + "tax", + "tattoo", + "tatar", + "tatamotors", + "target", + "taobao", + "talk", + "taipei", + "tab", + "systems", + "symantec", + "sydney", + "swiss", + "swiftcover", + "swatch", + "suzuki", + "surgery", + "surf", + "support", + "supply", + "supplies", + "sucks", + "style", + "study", + "studio", + "stream", + "store", + "storage", + "stockholm", + "stcgroup", + "stc", + "statoil", + "statefarm", + "statebank", + "starhub", + "star", + "staples", + "stada", + "srt", + "srl", + "spreadbetting", + "spot", + "sport", + "spiegel", + "space", + "soy", + "sony", + "song", + "solutions", + "solar", + "sohu", + "software", + "softbank", + "social", + "soccer", + "sncf", + "smile", + "smart", + "sling", + "skype", + "sky", + "skin", + "ski", + "site", + "singles", + "sina", + "silk", + "shriram", + "showtime", + "show", + "shouji", + "shopping", + "shop", + "shoes", + "shiksha", + "shia", + "shell", + "shaw", + "sharp", + "shangrila", + "sfr", + "sexy", + "sex", + "sew", + "seven", + "ses", + "services", + "sener", + "select", + "seek", + "security", + "secure", + "seat", + "search", + "scot", + "scor", + "scjohnson", + "science", + "schwarz", + "schule", + "school", + "scholarships", + "schmidt", + "schaeffler", + "scb", + "sca", + "sbs", + "sbi", + "saxo", + "save", + "sas", + "sarl", + "sapo", + "sap", + "sanofi", + "sandvikcoromant", + "sandvik", + "samsung", + "samsclub", + "salon", + "sale", + "sakura", + "safety", + "safe", + "saarland", + "ryukyu", + "rwe", + "run", + "ruhr", + "rugby", + "rsvp", + "room", + "rogers", + "rodeo", + "rocks", + "rocher", + "rmit", + "rip", + "rio", + "ril", + "rightathome", + "ricoh", + "richardli", + "rich", + "rexroth", + "reviews", + "review", + "restaurant", + "rest", + "republican", + "report", + "repair", + "rentals", + "rent", + "ren", + "reliance", + "reit", + "reisen", + "reise", + "rehab", + "redumbrella", + "redstone", + "red", + "recipes", + "realty", + "realtor", + "realestate", + "read", + "raid", + "radio", + "racing", + "qvc", + "quest", + "quebec", + "qpon", + "pwc", + "pub", + "prudential", + "pru", + "protection", + "property", + "properties", + "promo", + "progressive", + "prof", + "productions", + "prod", + "pro", + "prime", + "press", + "praxi", + "pramerica", + "post", + "porn", + "politie", + "poker", + "pohl", + "pnc", + "plus", + "plumbing", + "playstation", + "play", + "place", + "pizza", + "pioneer", + "pink", + "ping", + "pin", + "pid", + "pictures", + "pictet", + "pics", + "piaget", + "physio", + "photos", + "photography", + "photo", + "phone", + "philips", + "phd", + "pharmacy", + "pfizer", + "pet", + "pccw", + "pay", + "passagens", + "party", + "parts", + "partners", + "pars", + "paris", + "panerai", + "panasonic", + "pamperedchef", + "page", + "ovh", + "ott", + "otsuka", + "osaka", + "origins", + "orientexpress", + "organic", + "org", + "orange", + "oracle", + "open", + "ooo", + "onyourside", + "online", + "onl", + "ong", + "one", + "omega", + "ollo", + "oldnavy", + "olayangroup", + "olayan", + "okinawa", + "office", + "off", + "observer", + "obi", + "nyc", + "ntt", + "nrw", + "nra", + "nowtv", + "nowruz", + "now", + "norton", + "northwesternmutual", + "nokia", + "nissay", + "nissan", + "ninja", + "nikon", + "nike", + "nico", + "nhk", + "ngo", + "nfl", + "nexus", + "nextdirect", + "next", + "news", + "newholland", + "new", + "neustar", + "network", + "netflix", + "netbank", + "net", + "nec", + "nba", + "navy", + "natura", + "nationwide", + "name", + "nagoya", + "nadex", + "nab", + "mutuelle", + "mutual", + "museum", + "mtr", + "mtpc", + "mtn", + "msd", + "movistar", + "movie", + "mov", + "motorcycles", + "moto", + "moscow", + "mortgage", + "mormon", + "mopar", + "montblanc", + "monster", + "money", + "monash", + "mom", + "moi", + "moe", + "moda", + "mobily", + "mobile", + "mobi", + "mma", + "mls", + "mlb", + "mitsubishi", + "mit", + "mint", + "mini", + "mil", + "microsoft", + "miami", + "metlife", + "merckmsd", + "meo", + "menu", + "men", + "memorial", + "meme", + "melbourne", + "meet", + "media", + "med", + "mckinsey", + "mcdonalds", + "mcd", + "mba", + "mattel", + "maserati", + "marshalls", + "marriott", + "markets", + "marketing", + "market", + "map", + "mango", + "management", + "man", + "makeup", + "maison", + "maif", + "madrid", + "macys", + "luxury", + "luxe", + "lupin", + "lundbeck", + "ltda", + "ltd", + "lplfinancial", + "lpl", + "love", + "lotto", + "lotte", + "london", + "lol", + "loft", + "locus", + "locker", + "loans", + "loan", + "llp", + "llc", + "lixil", + "living", + "live", + "lipsy", + "link", + "linde", + "lincoln", + "limo", + "limited", + "lilly", + "like", + "lighting", + "lifestyle", + "lifeinsurance", + "life", + "lidl", + "liaison", + "lgbt", + "lexus", + "lego", + "legal", + "lefrak", + "leclerc", + "lease", + "lds", + "lawyer", + "law", + "latrobe", + "latino", + "lat", + "lasalle", + "lanxess", + "landrover", + "land", + "lancome", + "lancia", + "lancaster", + "lamer", + "lamborghini", + "ladbrokes", + "lacaixa", + "kyoto", + "kuokgroup", + "kred", + "krd", + "kpn", + "kpmg", + "kosher", + "komatsu", + "koeln", + "kiwi", + "kitchen", + "kindle", + "kinder", + "kim", + "kia", + "kfh", + "kerryproperties", + "kerrylogistics", + "kerryhotels", + "kddi", + "kaufen", + "juniper", + "juegos", + "jprs", + "jpmorgan", + "joy", + "jot", + "joburg", + "jobs", + "jnj", + "jmp", + "jll", + "jlc", + "jio", + "jewelry", + "jetzt", + "jeep", + "jcp", + "jcb", + "java", + "jaguar", + "iwc", + "iveco", + "itv", + "itau", + "istanbul", + "ist", + "ismaili", + "iselect", + "irish", + "ipiranga", + "investments", + "intuit", + "international", + "intel", + "int", + "insure", + "insurance", + "institute", + "ink", + "ing", + "info", + "infiniti", + "industries", + "inc", + "immobilien", + "immo", + "imdb", + "imamat", + "ikano", + "iinet", + "ifm", + "ieee", + "icu", + "ice", + "icbc", + "ibm", + "hyundai", + "hyatt", + "hughes", + "htc", + "hsbc", + "how", + "house", + "hotmail", + "hotels", + "hoteles", + "hot", + "hosting", + "host", + "hospital", + "horse", + "honeywell", + "honda", + "homesense", + "homes", + "homegoods", + "homedepot", + "holiday", + "holdings", + "hockey", + "hkt", + "hiv", + "hitachi", + "hisamitsu", + "hiphop", + "hgtv", + "hermes", + "here", + "helsinki", + "help", + "healthcare", + "health", + "hdfcbank", + "hdfc", + "hbo", + "haus", + "hangout", + "hamburg", + "hair", + "guru", + "guitars", + "guide", + "guge", + "gucci", + "guardian", + "group", + "grocery", + "gripe", + "green", + "gratis", + "graphics", + "grainger", + "gov", + "got", + "gop", + "google", + "goog", + "goodyear", + "goodhands", + "goo", + "golf", + "goldpoint", + "gold", + "godaddy", + "gmx", + "gmo", + "gmbh", + "gmail", + "globo", + "global", + "gle", + "glass", + "glade", + "giving", + "gives", + "gifts", + "gift", + "ggee", + "george", + "genting", + "gent", + "gea", + "gdn", + "gbiz", + "gay", + "garden", + "gap", + "games", + "game", + "gallup", + "gallo", + "gallery", + "gal", + "fyi", + "futbol", + "furniture", + "fund", + "fun", + "fujixerox", + "fujitsu", + "ftr", + "frontier", + "frontdoor", + "frogans", + "frl", + "fresenius", + "free", + "fox", + "foundation", + "forum", + "forsale", + "forex", + "ford", + "football", + "foodnetwork", + "food", + "foo", + "fly", + "flsmidth", + "flowers", + "florist", + "flir", + "flights", + "flickr", + "fitness", + "fit", + "fishing", + "fish", + "firmdale", + "firestone", + "fire", + "financial", + "finance", + "final", + "film", + "fido", + "fidelity", + "fiat", + "ferrero", + "ferrari", + "feedback", + "fedex", + "fast", + "fashion", + "farmers", + "farm", + "fans", + "fan", + "family", + "faith", + "fairwinds", + "fail", + "fage", + "extraspace", + "express", + "exposed", + "expert", + "exchange", + "everbank", + "events", + "eus", + "eurovision", + "etisalat", + "esurance", + "estate", + "esq", + "erni", + "ericsson", + "equipment", + "epson", + "epost", + "enterprises", + "engineering", + "engineer", + "energy", + "emerck", + "email", + "education", + "edu", + "edeka", + "eco", + "eat", + "earth", + "dvr", + "dvag", + "durban", + "dupont", + "duns", + "dunlop", + "duck", + "dubai", + "dtv", + "drive", + "download", + "dot", + "doosan", + "domains", + "doha", + "dog", + "dodge", + "doctor", + "docs", + "dnp", + "diy", + "dish", + "discover", + "discount", + "directory", + "direct", + "digital", + "diet", + "diamonds", + "dhl", + "dev", + "design", + "desi", + "dentist", + "dental", + "democrat", + "delta", + "deloitte", + "dell", + "delivery", + "degree", + "deals", + "dealer", + "deal", + "dds", + "dclk", + "day", + "datsun", + "dating", + "date", + "data", + "dance", + "dad", + "dabur", + "cyou", + "cymru", + "cuisinella", + "csc", + "cruises", + "cruise", + "crs", + "crown", + "cricket", + "creditunion", + "creditcard", + "credit", + "cpa", + "courses", + "coupons", + "coupon", + "country", + "corsica", + "coop", + "cool", + "cookingchannel", + "cooking", + "contractors", + "contact", + "consulting", + "construction", + "condos", + "comsec", + "computer", + "compare", + "company", + "community", + "commbank", + "comcast", + "com", + "cologne", + "college", + "coffee", + "codes", + "coach", + "clubmed", + "club", + "cloud", + "clothing", + "clinique", + "clinic", + "click", + "cleaning", + "claims", + "cityeats", + "city", + "citic", + "citi", + "citadel", + "cisco", + "circle", + "cipriani", + "church", + "chrysler", + "chrome", + "christmas", + "chloe", + "chintai", + "cheap", + "chat", + "chase", + "charity", + "channel", + "chanel", + "cfd", + "cfa", + "cern", + "ceo", + "center", + "ceb", + "cbs", + "cbre", + "cbn", + "cba", + "catholic", + "catering", + "cat", + "casino", + "cash", + "caseih", + "case", + "casa", + "cartier", + "cars", + "careers", + "career", + "care", + "cards", + "caravan", + "car", + "capitalone", + "capital", + "capetown", + "canon", + "cancerresearch", + "camp", + "camera", + "cam", + "calvinklein", + "call", + "cal", + "cafe", + "cab", + "bzh", + "buzz", + "buy", + "business", + "builders", + "build", + "bugatti", + "budapest", + "brussels", + "brother", + "broker", + "broadway", + "bridgestone", + "bradesco", + "box", + "boutique", + "bot", + "boston", + "bostik", + "bosch", + "boots", + "booking", + "book", + "boo", + "bond", + "bom", + "bofa", + "boehringer", + "boats", + "bnpparibas", + "bnl", + "bmw", + "bms", + "blue", + "bloomberg", + "blog", + "blockbuster", + "blanco", + "blackfriday", + "black", + "biz", + "bio", + "bingo", + "bing", + "bike", + "bid", + "bible", + "bharti", + "bet", + "bestbuy", + "best", + "berlin", + "bentley", + "beer", + "beauty", + "beats", + "bcn", + "bcg", + "bbva", + "bbt", + "bbc", + "bayern", + "bauhaus", + "basketball", + "baseball", + "bargains", + "barefoot", + "barclays", + "barclaycard", + "barcelona", + "bar", + "bank", + "band", + "bananarepublic", + "banamex", + "baidu", + "baby", + "azure", + "axa", + "aws", + "avianca", + "autos", + "auto", + "author", + "auspost", + "audio", + "audible", + "audi", + "auction", + "attorney", + "athleta", + "associates", + "asia", + "asda", + "arte", + "art", + "arpa", + "army", + "archi", + "aramco", + "arab", + "aquarelle", + "apple", + "app", + "apartments", + "aol", + "anz", + "anquan", + "android", + "analytics", + "amsterdam", + "amica", + "amfam", + "amex", + "americanfamily", + "americanexpress", + "alstom", + "alsace", + "ally", + "allstate", + "allfinanz", + "alipay", + "alibaba", + "alfaromeo", + "akdn", + "airtel", + "airforce", + "airbus", + "aigo", + "aig", + "agency", + "agakhan", + "africa", + "afl", + "afamilycompany", + "aetna", + "aero", + "aeg", + "adult", + "ads", + "adac", + "actor", + "active", + "aco", + "accountants", + "accountant", + "accenture", + "academy", + "abudhabi", + "abogado", + "able", + "abc", + "abbvie", + "abbott", + "abb", + "abarth", + "aarp", + "aaa", + "onion" + ); + + public static final List CTLDS = Arrays.asList( + "한국", + "香港", + "澳門", + "新加坡", + "台灣", + "台湾", + "中國", + "中国", + "გე", + "ລາວ", + "ไทย", + "ලංකා", + "ഭാരതം", + "ಭಾರತ", + "భారత్", + "சிங்கப்பூர்", + "இலங்கை", + "இந்தியா", + "ଭାରତ", + "ભારત", + "ਭਾਰਤ", + "ভাৰত", + "ভারত", + "বাংলা", + "भारोत", + "भारतम्", + "भारत", + "ڀارت", + "پاکستان", + "موريتانيا", + "مليسيا", + "مصر", + "قطر", + "فلسطين", + "عمان", + "عراق", + "سورية", + "سودان", + "تونس", + "بھارت", + "بارت", + "ایران", + "امارات", + "المغرب", + "السعودية", + "الجزائر", + "البحرين", + "الاردن", + "հայ", + "қаз", + "укр", + "срб", + "рф", + "мон", + "мкд", + "ею", + "бел", + "бг", + "ευ", + "ελ", + "zw", + "zm", + "za", + "yt", + "ye", + "ws", + "wf", + "vu", + "vn", + "vi", + "vg", + "ve", + "vc", + "va", + "uz", + "uy", + "us", + "um", + "uk", + "ug", + "ua", + "tz", + "tw", + "tv", + "tt", + "tr", + "tp", + "to", + "tn", + "tm", + "tl", + "tk", + "tj", + "th", + "tg", + "tf", + "td", + "tc", + "sz", + "sy", + "sx", + "sv", + "su", + "st", + "ss", + "sr", + "so", + "sn", + "sm", + "sl", + "sk", + "sj", + "si", + "sh", + "sg", + "se", + "sd", + "sc", + "sb", + "sa", + "rw", + "ru", + "rs", + "ro", + "re", + "qa", + "py", + "pw", + "pt", + "ps", + "pr", + "pn", + "pm", + "pl", + "pk", + "ph", + "pg", + "pf", + "pe", + "pa", + "om", + "nz", + "nu", + "nr", + "np", + "no", + "nl", + "ni", + "ng", + "nf", + "ne", + "nc", + "na", + "mz", + "my", + "mx", + "mw", + "mv", + "mu", + "mt", + "ms", + "mr", + "mq", + "mp", + "mo", + "mn", + "mm", + "ml", + "mk", + "mh", + "mg", + "mf", + "me", + "md", + "mc", + "ma", + "ly", + "lv", + "lu", + "lt", + "ls", + "lr", + "lk", + "li", + "lc", + "lb", + "la", + "kz", + "ky", + "kw", + "kr", + "kp", + "kn", + "km", + "ki", + "kh", + "kg", + "ke", + "jp", + "jo", + "jm", + "je", + "it", + "is", + "ir", + "iq", + "io", + "in", + "im", + "il", + "ie", + "id", + "hu", + "ht", + "hr", + "hn", + "hm", + "hk", + "gy", + "gw", + "gu", + "gt", + "gs", + "gr", + "gq", + "gp", + "gn", + "gm", + "gl", + "gi", + "gh", + "gg", + "gf", + "ge", + "gd", + "gb", + "ga", + "fr", + "fo", + "fm", + "fk", + "fj", + "fi", + "eu", + "et", + "es", + "er", + "eh", + "eg", + "ee", + "ec", + "dz", + "do", + "dm", + "dk", + "dj", + "de", + "cz", + "cy", + "cx", + "cw", + "cv", + "cu", + "cr", + "co", + "cn", + "cm", + "cl", + "ck", + "ci", + "ch", + "cg", + "cf", + "cd", + "cc", + "ca", + "bz", + "by", + "bw", + "bv", + "bt", + "bs", + "br", + "bq", + "bo", + "bn", + "bm", + "bl", + "bj", + "bi", + "bh", + "bg", + "bf", + "be", + "bd", + "bb", + "ba", + "az", + "ax", + "aw", + "au", + "at", + "as", + "ar", + "aq", + "ao", + "an", + "am", + "al", + "ai", + "ag", + "af", + "ae", + "ad", + "ac" + ); +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java index 6451897f..bbc96c66 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java @@ -1,6 +1,8 @@ package org.joinmastodon.android.fragments; import android.app.Activity; +import android.graphics.Canvas; +import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Bundle; @@ -9,8 +11,10 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; +import org.joinmastodon.android.R; import org.joinmastodon.android.model.DisplayItemsParent; import org.joinmastodon.android.model.Status; +import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.PhotoStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.photoviewer.PhotoViewer; @@ -26,6 +30,7 @@ import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter; import me.grishka.appkit.imageloader.ImageLoaderViewHolder; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; import me.grishka.appkit.utils.BindableViewHolder; +import me.grishka.appkit.utils.V; import me.grishka.appkit.views.UsableRecyclerView; public abstract class BaseStatusListFragment extends BaseRecyclerFragment implements PhotoViewerHost{ @@ -204,6 +209,26 @@ public abstract class BaseStatusListFragment exten currentPhotoViewer.offsetView(-dx, -dy); } }); + list.addItemDecoration(new RecyclerView.ItemDecoration(){ + private Paint paint=new Paint(); + { + paint.setColor(0xFFD0D5DD); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(V.dp(1)); + } + + @Override + public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){ + for(int i=0;i{ + mainEditText.requestFocus(); + imm.showSoftInput(mainEditText, 0); + }, 100); + + mainEditText.addTextChangedListener(new TextWatcher(){ + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after){ + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count){ + + } + + @Override + public void afterTextChanged(Editable s){ + updateCharCounter(s); + } + }); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){ + publishButton=menu.add("TOOT!"); + publishButton.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + updatePublishButtonState(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item){ + String text=mainEditText.getText().toString(); + CreateStatus.Request req=new CreateStatus.Request(); + req.status=text; + String uuid=UUID.randomUUID().toString(); + new CreateStatus(req, uuid) + .setCallback(new Callback<>(){ + @Override + public void onSuccess(Status result){ + Nav.finish(ComposeFragment.this); + E.post(new StatusCreatedEvent(result)); + } + + @Override + public void onError(ErrorResponse error){ + error.showToast(getActivity()); + } + }) + .exec(accountID); + return true; + } + + private void updateCharCounter(CharSequence text){ + String countableText=MENTION_PATTERN.matcher(URL_PATTERN.matcher(text).replaceAll("$2xxxxxxxxxxxxxxxxxxxxxxx")).replaceAll("$1@$3"); + breakIterator.setText(countableText); + charCount=0; + while(breakIterator.next()!=BreakIterator.DONE){ + charCount++; + } + + charCounter.setText(String.valueOf(charLimit-charCount)); + updatePublishButtonState(); + } + + private void updatePublishButtonState(){ + publishButton.setEnabled(charCount>0 && charCount<=charLimit); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/CreateTootFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/CreateTootFragment.java deleted file mode 100644 index 2d140a8f..00000000 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/CreateTootFragment.java +++ /dev/null @@ -1,88 +0,0 @@ -package org.joinmastodon.android.fragments; - -import android.app.Activity; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.InputMethodManager; -import android.widget.EditText; - -import org.joinmastodon.android.E; -import org.joinmastodon.android.R; -import org.joinmastodon.android.api.requests.statuses.CreateStatus; -import org.joinmastodon.android.events.StatusCreatedEvent; -import org.joinmastodon.android.model.Status; - -import java.util.UUID; - -import me.grishka.appkit.Nav; -import me.grishka.appkit.api.Callback; -import me.grishka.appkit.api.ErrorResponse; -import me.grishka.appkit.fragments.ToolbarFragment; - -public class CreateTootFragment extends ToolbarFragment{ - - private EditText mainEditText; - private String accountID; - - @Override - public void onAttach(Activity activity){ - super.onAttach(activity); - setHasOptionsMenu(true); - accountID=getArguments().getString("account"); - } - - @Override - public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){ - View view=inflater.inflate(R.layout.fragment_new_toot, container, false); - mainEditText=view.findViewById(R.id.toot_text); - return view; - } - - @Override - public void onResume(){ - super.onResume(); - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState){ - super.onViewCreated(view, savedInstanceState); - InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class); - view.postDelayed(()->{ - mainEditText.requestFocus(); - imm.showSoftInput(mainEditText, 0); - }, 100); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){ - menu.add("TOOT!").setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item){ - String text=mainEditText.getText().toString(); - CreateStatus.Request req=new CreateStatus.Request(); - req.status=text; - String uuid=UUID.randomUUID().toString(); - new CreateStatus(req, uuid) - .setCallback(new Callback<>(){ - @Override - public void onSuccess(Status result){ - Nav.finish(CreateTootFragment.this); - E.post(new StatusCreatedEvent(result)); - } - - @Override - public void onError(ErrorResponse error){ - error.showToast(getActivity()); - } - }) - .exec(accountID); - return true; - } -} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java index ee1552e3..697e514a 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java @@ -49,4 +49,9 @@ public class HomeFragment extends AppKitFragment{ super.onHiddenChanged(hidden); homeTimelineFragment.onHiddenChanged(hidden); } + + @Override + public boolean wantsLightStatusBar(){ + return true; + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTimelineFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTimelineFragment.java index effe7d9f..0030799b 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTimelineFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTimelineFragment.java @@ -5,6 +5,8 @@ import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import android.view.View; +import android.widget.ImageButton; import com.squareup.otto.Subscribe; @@ -23,6 +25,11 @@ import me.grishka.appkit.Nav; import me.grishka.appkit.api.SimpleCallback; public class HomeTimelineFragment extends StatusListFragment{ + private ImageButton fab; + + public HomeTimelineFragment(){ + setListLayoutId(R.layout.recycler_fragment_with_fab); + } @Override public void onAttach(Activity activity){ @@ -44,6 +51,13 @@ public class HomeTimelineFragment extends StatusListFragment{ .exec(accountID); } + @Override + public void onViewCreated(View view, Bundle savedInstanceState){ + super.onViewCreated(view, savedInstanceState); + fab=view.findViewById(R.id.fab); + fab.setOnClickListener(this::onFabClick); + } + @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){ inflater.inflate(R.menu.home, menu); @@ -55,7 +69,7 @@ public class HomeTimelineFragment extends StatusListFragment{ args.putString("account", accountID); int id=item.getItemId(); if(id==R.id.new_toot){ - Nav.go(getActivity(), CreateTootFragment.class, args); + Nav.go(getActivity(), ComposeFragment.class, args); }else if(id==R.id.notifications){ Nav.go(getActivity(), NotificationsFragment.class, args); }else if(id==R.id.my_profile){ @@ -81,4 +95,10 @@ public class HomeTimelineFragment extends StatusListFragment{ public void onStatusCreated(StatusCreatedEvent ev){ prependItems(Collections.singletonList(ev.status)); } + + private void onFabClick(View v){ + Bundle args=new Bundle(); + args.putString("account", accountID); + Nav.go(getActivity(), ComposeFragment.class, args); + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java new file mode 100644 index 00000000..c68509ad --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/FooterStatusDisplayItem.java @@ -0,0 +1,68 @@ +package org.joinmastodon.android.ui.displayitems; + +import android.app.Activity; +import android.content.res.ColorStateList; +import android.os.Build; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import org.joinmastodon.android.R; +import org.joinmastodon.android.model.Status; +import org.joinmastodon.android.ui.utils.UiUtils; + +import java.text.DecimalFormat; + +import me.grishka.appkit.utils.BindableViewHolder; +import me.grishka.appkit.utils.V; + +public class FooterStatusDisplayItem extends StatusDisplayItem{ + private final Status status; + private final String accountID; + + public FooterStatusDisplayItem(String parentID, Status status, String accountID){ + super(parentID); + this.status=status; + this.accountID=accountID; + } + + @Override + public Type getType(){ + return Type.FOOTER; + } + + public static class Holder extends BindableViewHolder{ + private final TextView reply, boost, favorite; + private final ImageView share; + + public Holder(Activity activity, ViewGroup parent){ + super(activity, R.layout.display_item_footer, parent); + reply=findViewById(R.id.reply); + boost=findViewById(R.id.boost); + favorite=findViewById(R.id.favorite); + share=findViewById(R.id.share); + if(Build.VERSION.SDK_INT0){ + btn.setText(DecimalFormat.getIntegerInstance().format(count)); + btn.setCompoundDrawablePadding(V.dp(8)); + }else{ + btn.setText(""); + btn.setCompoundDrawablePadding(0); + } + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java index 65b26f72..81bf35c6 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java @@ -2,18 +2,21 @@ package org.joinmastodon.android.ui.displayitems; import android.app.Activity; import android.app.Fragment; +import android.graphics.Outline; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; +import android.os.Build; import android.os.Bundle; import android.view.View; import android.view.ViewGroup; +import android.view.ViewOutlineProvider; import android.widget.ImageView; import android.widget.TextView; import org.joinmastodon.android.R; import org.joinmastodon.android.fragments.ProfileFragment; import org.joinmastodon.android.model.Account; -import org.joinmastodon.android.model.Status; +import org.joinmastodon.android.ui.utils.UiUtils; import org.parceler.Parcels; import java.time.Instant; @@ -23,6 +26,7 @@ import me.grishka.appkit.imageloader.ImageLoaderViewHolder; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; import me.grishka.appkit.utils.BindableViewHolder; +import me.grishka.appkit.utils.V; public class HeaderStatusDisplayItem extends StatusDisplayItem{ private Account user; @@ -56,20 +60,34 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{ } public static class Holder extends BindableViewHolder implements ImageLoaderViewHolder{ - private final TextView name, subtitle; - private final ImageView avatar; + private final TextView name, username, timestamp; + private final ImageView avatar, more; + + private static final ViewOutlineProvider roundCornersOutline=new ViewOutlineProvider(){ + @Override + public void getOutline(View view, Outline outline){ + outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), V.dp(12)); + } + }; + public Holder(Activity activity, ViewGroup parent){ super(activity, R.layout.display_item_header, parent); name=findViewById(R.id.name); - subtitle=findViewById(R.id.subtitle); + username=findViewById(R.id.username); + timestamp=findViewById(R.id.timestamp); avatar=findViewById(R.id.avatar); + more=findViewById(R.id.more); avatar.setOnClickListener(this::onAvaClick); + avatar.setOutlineProvider(roundCornersOutline); + avatar.setClipToOutline(true); + more.setOnClickListener(this::onMoreClick); } @Override public void onBind(HeaderStatusDisplayItem item){ name.setText(item.user.displayName); - subtitle.setText('@'+item.user.acct); + username.setText('@'+item.user.acct); + timestamp.setText(UiUtils.formatRelativeTimestamp(itemView.getContext(), item.createdAt)); } @Override @@ -90,5 +108,9 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{ args.putParcelable("profileAccount", Parcels.wrap(item.user)); Nav.go(item.parentFragment.getActivity(), ProfileFragment.class, args); } + + private void onMoreClick(View v){ + + } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ReblogOrReplyLineStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ReblogOrReplyLineStatusDisplayItem.java index 3002ea07..b48df959 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ReblogOrReplyLineStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ReblogOrReplyLineStatusDisplayItem.java @@ -1,11 +1,12 @@ package org.joinmastodon.android.ui.displayitems; import android.app.Activity; +import android.os.Build; import android.view.ViewGroup; import android.widget.TextView; import org.joinmastodon.android.R; -import org.joinmastodon.android.model.Status; +import org.joinmastodon.android.ui.utils.UiUtils; import me.grishka.appkit.utils.BindableViewHolder; @@ -27,6 +28,8 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{ public Holder(Activity activity, ViewGroup parent){ super(activity, R.layout.display_item_reblog_or_reply_line, parent); text=findViewById(R.id.text); + if(Build.VERSION.SDK_INT new ReblogOrReplyLineStatusDisplayItem.Holder(activity, parent); case TEXT -> new TextStatusDisplayItem.Holder(activity, parent); case PHOTO -> new PhotoStatusDisplayItem.Holder(activity, parent); + case FOOTER -> new FooterStatusDisplayItem.Holder(activity, parent); default -> throw new UnsupportedOperationException(); }; } @@ -58,6 +59,7 @@ public abstract class StatusDisplayItem{ items.add(new PhotoStatusDisplayItem(parentID, status, attachment, fragment)); } } + items.add(new FooterStatusDisplayItem(parentID, status, accountID)); return items; } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java index d4b4d0a5..14f46f85 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java @@ -1,8 +1,17 @@ package org.joinmastodon.android.ui.utils; import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.drawable.Drawable; import android.net.Uri; +import android.util.Log; +import android.widget.TextView; +import org.joinmastodon.android.R; + +import java.time.Instant; + +import androidx.annotation.ColorRes; import androidx.browser.customtabs.CustomTabsIntent; public class UiUtils{ @@ -14,4 +23,37 @@ public class UiUtils{ .build() .launchUrl(context, Uri.parse(url)); } + + public static String formatRelativeTimestamp(Context context, Instant instant){ + long t=instant.toEpochMilli(); + long now=System.currentTimeMillis(); + long diff=now-t; + if(diff<60_000L){ + return context.getString(R.string.time_seconds, diff/1000L); + }else if(diff<3600_000L){ + return context.getString(R.string.time_minutes, diff/60_000L); + }else if(diff<3600_000L*24L){ + return context.getString(R.string.time_hours, diff/3600_000L); + }else{ + return context.getString(R.string.time_days, diff/(3600_000L*24L)); + } + } + + /** + * Android 6.0 has a bug where start and end compound drawables don't get tinted. + * This works around it by setting the tint colors directly to the drawables. + * @param textView + * @param color + */ + public static void fixCompoundDrawableTintOnAndroid6(TextView textView, @ColorRes int color){ + Drawable[] drawables=textView.getCompoundDrawablesRelative(); + for(int i=0;i1){ + int remainingWidth=MeasureSpec.getSize(widthMeasureSpec); + for(int i=1;i + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/bg_fab.xml b/mastodon/src/main/res/drawable/bg_fab.xml new file mode 100644 index 00000000..cbfc5b18 --- /dev/null +++ b/mastodon/src/main/res/drawable/bg_fab.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/ic_edit_34.xml b/mastodon/src/main/res/drawable/ic_edit_34.xml new file mode 100644 index 00000000..9e7fa0dd --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_edit_34.xml @@ -0,0 +1,5 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_fluent_arrow_repeat_all_20_filled.xml b/mastodon/src/main/res/drawable/ic_fluent_arrow_repeat_all_20_filled.xml new file mode 100644 index 00000000..54120669 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_arrow_repeat_all_20_filled.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_fluent_arrow_repeat_all_24_regular.xml b/mastodon/src/main/res/drawable/ic_fluent_arrow_repeat_all_24_regular.xml new file mode 100644 index 00000000..27d5fc48 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_arrow_repeat_all_24_regular.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_fluent_chat_multiple_24_regular.xml b/mastodon/src/main/res/drawable/ic_fluent_chat_multiple_24_regular.xml new file mode 100644 index 00000000..dc56f5bd --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_chat_multiple_24_regular.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_fluent_share_24_regular.xml b/mastodon/src/main/res/drawable/ic_fluent_share_24_regular.xml new file mode 100644 index 00000000..19b179b2 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_share_24_regular.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_fluent_star_24_filled.xml b/mastodon/src/main/res/drawable/ic_fluent_star_24_filled.xml new file mode 100644 index 00000000..4d6e87ce --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_star_24_filled.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_fluent_star_24_regular.xml b/mastodon/src/main/res/drawable/ic_fluent_star_24_regular.xml new file mode 100644 index 00000000..8392e35a --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_star_24_regular.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_post_more.xml b/mastodon/src/main/res/drawable/ic_post_more.xml new file mode 100644 index 00000000..a1bef7ea --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_post_more.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/layout/display_item_footer.xml b/mastodon/src/main/res/layout/display_item_footer.xml new file mode 100644 index 00000000..388e6564 --- /dev/null +++ b/mastodon/src/main/res/layout/display_item_footer.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/display_item_header.xml b/mastodon/src/main/res/layout/display_item_header.xml index b84f949e..878b63c6 100644 --- a/mastodon/src/main/res/layout/display_item_header.xml +++ b/mastodon/src/main/res/layout/display_item_header.xml @@ -3,32 +3,73 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:padding="8dp"> + android:paddingTop="16dp" + android:paddingRight="16dp" + android:paddingLeft="16dp"> + + + android:layout_alignParentTop="true" + android:layout_marginEnd="12dp" /> + android:singleLine="true" + android:textAppearance="@style/m3_title_medium" + tools:text="Eugen" /> - + android:layoutDirection="locale" + android:orientation="horizontal"> + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/display_item_reblog_or_reply_line.xml b/mastodon/src/main/res/layout/display_item_reblog_or_reply_line.xml index 9c1b302a..3643b9ab 100644 --- a/mastodon/src/main/res/layout/display_item_reblog_or_reply_line.xml +++ b/mastodon/src/main/res/layout/display_item_reblog_or_reply_line.xml @@ -2,12 +2,19 @@ + android:layout_marginBottom="-6dp" + android:paddingLeft="16dp" + android:paddingRight="16dp" + android:paddingTop="16dp"> diff --git a/mastodon/src/main/res/layout/display_item_text.xml b/mastodon/src/main/res/layout/display_item_text.xml index 46c46b9c..d5b95a54 100644 --- a/mastodon/src/main/res/layout/display_item_text.xml +++ b/mastodon/src/main/res/layout/display_item_text.xml @@ -2,11 +2,15 @@ + android:paddingLeft="16dp" + android:paddingRight="16dp" + android:paddingTop="10dp" + android:paddingBottom="12dp"> + android:layout_height="wrap_content" + android:textAppearance="@style/m3_body_large"/> \ No newline at end of file diff --git a/mastodon/src/main/res/layout/fragment_compose.xml b/mastodon/src/main/res/layout/fragment_compose.xml new file mode 100644 index 00000000..e7be0378 --- /dev/null +++ b/mastodon/src/main/res/layout/fragment_compose.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/fragment_new_toot.xml b/mastodon/src/main/res/layout/fragment_new_toot.xml deleted file mode 100644 index 74172bc0..00000000 --- a/mastodon/src/main/res/layout/fragment_new_toot.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/mastodon/src/main/res/layout/recycler_fragment_with_fab.xml b/mastodon/src/main/res/layout/recycler_fragment_with_fab.xml new file mode 100644 index 00000000..a908ee9a --- /dev/null +++ b/mastodon/src/main/res/layout/recycler_fragment_with_fab.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/values/colors.xml b/mastodon/src/main/res/values/colors.xml index d95bf93e..de2144dd 100644 --- a/mastodon/src/main/res/values/colors.xml +++ b/mastodon/src/main/res/values/colors.xml @@ -7,4 +7,15 @@ #FF018786 #FF000000 #FFFFFFFF + + @color/gray_800 + + #282C37 + #667085 + #CCF9FAFB + + @color/gray_800 + @color/gray_500 + #E9EDF2 + #282C37 \ No newline at end of file diff --git a/mastodon/src/main/res/values/strings.xml b/mastodon/src/main/res/values/strings.xml index 49d2675c..95c7e8d7 100644 --- a/mastodon/src/main/res/values/strings.xml +++ b/mastodon/src/main/res/values/strings.xml @@ -20,4 +20,9 @@ %s favorited your toot Poll you voted in has ended %s posted + + %ds + %dm + %dh + %dd \ No newline at end of file diff --git a/mastodon/src/main/res/values/styles.xml b/mastodon/src/main/res/values/styles.xml index d30396ee..b248104e 100644 --- a/mastodon/src/main/res/values/styles.xml +++ b/mastodon/src/main/res/values/styles.xml @@ -4,5 +4,30 @@ false false + true + @color/white + + + + + + + + + \ No newline at end of file