Better char counter and custom emoji in compose
This commit is contained in:
parent
b9bdf7caec
commit
c885a5fc28
|
@ -0,0 +1,194 @@
|
|||
//
|
||||
// TwitterTextEmojiRegex.java
|
||||
//
|
||||
// Copyright 2018 Twitter, Inc.
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// DO NOT MODIFY THIS FILE -- it is generated code
|
||||
package com.twitter.twittertext;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public abstract class TwitterTextEmojiRegex {
|
||||
// This regex attempts to match all known Unicode Emoji sequences as specified by
|
||||
// http://www.unicode.org/reports/tr51/
|
||||
|
||||
// The goal is to parse them in the way iOS, Google, and others actually encode Emoji
|
||||
// using variant selectors, skin tones, regional symbols, zero width joiners, etc..
|
||||
|
||||
private static final String EMOJI =
|
||||
// Zero Width Joiners need to be ahead of other Emoji to get priority
|
||||
"(?:\ud83d\udc68\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68[\ud83c\udffc-\ud83c\udfff]" +
|
||||
"|\ud83d\udc68\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68[\ud83c\udffb\ud83c\udffd-" +
|
||||
"\ud83c\udfff]|\ud83d\udc68\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68[\ud83c\udffb" +
|
||||
"\ud83c\udffc\ud83c\udffe\ud83c\udfff]|\ud83d\udc68\ud83c\udffe\u200d\ud83e\udd1d\u200d" +
|
||||
"\ud83d\udc68[\ud83c\udffb-\ud83c\udffd\ud83c\udfff]|\ud83d\udc68\ud83c\udfff\u200d\ud83e" +
|
||||
"\udd1d\u200d\ud83d\udc68[\ud83c\udffb-\ud83c\udffe]|\ud83d\udc69\ud83c\udffb\u200d\ud83e" +
|
||||
"\udd1d\u200d\ud83d\udc68[\ud83c\udffc-\ud83c\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e" +
|
||||
"\udd1d\u200d\ud83d\udc69[\ud83c\udffc-\ud83c\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e" +
|
||||
"\udd1d\u200d\ud83d\udc68[\ud83c\udffb\ud83c\udffd-\ud83c\udfff]|\ud83d\udc69\ud83c\udffc" +
|
||||
"\u200d\ud83e\udd1d\u200d\ud83d\udc69[\ud83c\udffb\ud83c\udffd-\ud83c\udfff]|\ud83d\udc69" +
|
||||
"\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68[\ud83c\udffb\ud83c\udffc\ud83c\udffe" +
|
||||
"\ud83c\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc69[\ud83c\udffb" +
|
||||
"\ud83c\udffc\ud83c\udffe\ud83c\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d" +
|
||||
"\ud83d\udc68[\ud83c\udffb-\ud83c\udffd\ud83c\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e" +
|
||||
"\udd1d\u200d\ud83d\udc69[\ud83c\udffb-\ud83c\udffd\ud83c\udfff]|\ud83d\udc69\ud83c\udfff" +
|
||||
"\u200d\ud83e\udd1d\u200d\ud83d\udc68[\ud83c\udffb-\ud83c\udffe]|\ud83d\udc69\ud83c\udfff" +
|
||||
"\u200d\ud83e\udd1d\u200d\ud83d\udc69[\ud83c\udffb-\ud83c\udffe]|\ud83e\uddd1\ud83c\udffb" +
|
||||
"\u200d\ud83e\udd1d\u200d\ud83e\uddd1[\ud83c\udffb-\ud83c\udfff]|\ud83e\uddd1\ud83c\udffc" +
|
||||
"\u200d\ud83e\udd1d\u200d\ud83e\uddd1[\ud83c\udffb-\ud83c\udfff]|\ud83e\uddd1\ud83c\udffd" +
|
||||
"\u200d\ud83e\udd1d\u200d\ud83e\uddd1[\ud83c\udffb-\ud83c\udfff]|\ud83e\uddd1\ud83c\udffe" +
|
||||
"\u200d\ud83e\udd1d\u200d\ud83e\uddd1[\ud83c\udffb-\ud83c\udfff]|\ud83e\uddd1\ud83c\udfff" +
|
||||
"\u200d\ud83e\udd1d\u200d\ud83e\uddd1[\ud83c\udffb-\ud83c\udfff]|\ud83e\uddd1\u200d\ud83e" +
|
||||
"\udd1d\u200d\ud83e\uddd1|\ud83d\udc6b[\ud83c\udffb-\ud83c\udfff]|\ud83d\udc6c[\ud83c\udffb" +
|
||||
"-\ud83c\udfff]|\ud83d\udc6d[\ud83c\udffb-\ud83c\udfff]|[\ud83d\udc6b-\ud83d\udc6d])" + "|" +
|
||||
|
||||
// Leading woman/man zwj with optional skin tone
|
||||
"[\ud83d\udc68\ud83d\udc69\ud83e\uddd1][\ud83c\udffb-\ud83c\udfff]?\u200d" +
|
||||
"(?:\u2695\ufe0f|\u2696\ufe0f|\u2708\ufe0f|[\ud83c\udf3e\ud83c\udf73\ud83c\udf93\ud83c" +
|
||||
"\udfa4\ud83c\udfa8\ud83c\udfeb\ud83c\udfed\ud83d\udcbb\ud83d\udcbc\ud83d\udd27\ud83d\udd2c" +
|
||||
"\ud83d\ude80\ud83d\ude92\ud83e\uddaf-\ud83e\uddb3\ud83e\uddbc\ud83e\uddbd])" + "|" +
|
||||
|
||||
// Variant or skin tone before trailing female/male zwj (+ a captured group)
|
||||
// The group provides a way to detect that the base has VS16 instead of skin tone
|
||||
"[\u26f9\ud83c\udfcb\ud83c\udfcc\ud83d\udd74\ud83d\udd75]" +
|
||||
"([\ufe0f\ud83c\udffb-\ud83c\udfff]\u200d[\u2640\u2642]\ufe0f)|" +
|
||||
|
||||
// Optional skin tone before trailing female/male zwj
|
||||
"[\ud83c\udfc3\ud83c\udfc4\ud83c\udfca\ud83d\udc6e\ud83d\udc71\ud83d\udc73\ud83d\udc77" +
|
||||
"\ud83d\udc81\ud83d\udc82\ud83d\udc86\ud83d\udc87\ud83d\ude45-\ud83d\ude47\ud83d\ude4b" +
|
||||
"\ud83d\ude4d\ud83d\ude4e\ud83d\udea3\ud83d\udeb4-\ud83d\udeb6\ud83e\udd26\ud83e\udd35" +
|
||||
"\ud83e\udd37-\ud83e\udd39\ud83e\udd3d\ud83e\udd3e\ud83e\uddb8\ud83e\uddb9\ud83e\uddcd-" +
|
||||
"\ud83e\uddcf\ud83e\uddd6-\ud83e\udddd]" +
|
||||
"[\ud83c\udffb-\ud83c\udfff]?\u200d[\u2640\u2642]\ufe0f|" +
|
||||
|
||||
// Other zwj sequences
|
||||
"(?:\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68|\ud83d\udc69\u200d" +
|
||||
"\u2764\ufe0f\u200d\ud83d\udc8b\u200d[\ud83d\udc68\ud83d\udc69]|\ud83d\udc68\u200d\ud83d" +
|
||||
"\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d" +
|
||||
"\udc67\u200d[\ud83d\udc66\ud83d\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66" +
|
||||
"\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d[\ud83d\udc66" +
|
||||
"\ud83d\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d" +
|
||||
"\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d[\ud83d\udc66\ud83d\udc67]|\ud83d\udc68" +
|
||||
"\u200d\u2764\ufe0f\u200d\ud83d\udc68|\ud83d\udc69\u200d\u2764\ufe0f\u200d[\ud83d\udc68" +
|
||||
"\ud83d\udc69]|\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f|\ud83d\udc68\u200d\ud83d\udc66\u200d" +
|
||||
"\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc67\u200d[\ud83d\udc66\ud83d\udc67]|\ud83d\udc68" +
|
||||
"\u200d\ud83d\udc68\u200d[\ud83d\udc66\ud83d\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d[" +
|
||||
"\ud83d\udc66\ud83d\udc67]|\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69" +
|
||||
"\u200d\ud83d\udc67\u200d[\ud83d\udc66\ud83d\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d[" +
|
||||
"\ud83d\udc66\ud83d\udc67]|\ud83c\udff3\ufe0f\u200d\ud83c\udf08|\ud83c\udff4\u200d\u2620" +
|
||||
"\ufe0f|\ud83d\udc6f\u200d\u2640\ufe0f|\ud83d\udc6f\u200d\u2642\ufe0f|\ud83e\udd3c\u200d" +
|
||||
"\u2640\ufe0f|\ud83e\udd3c\u200d\u2642\ufe0f|\ud83e\uddde\u200d\u2640\ufe0f|\ud83e\uddde" +
|
||||
"\u200d\u2642\ufe0f|\ud83e\udddf\u200d\u2640\ufe0f|\ud83e\udddf\u200d\u2642\ufe0f|\ud83d" +
|
||||
"\udc15\u200d\ud83e\uddba|\ud83d\udc41\u200d\ud83d\udde8|\ud83d\udc68\u200d[\ud83d\udc66" +
|
||||
"\ud83d\udc67]|\ud83d\udc69\u200d[\ud83d\udc66\ud83d\udc67])" + "|" +
|
||||
|
||||
// Emojified symbols #, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 + possible variant selector + keycap
|
||||
"[#*0-9]" +
|
||||
"\ufe0f?\u20e3|" +
|
||||
|
||||
// Emoji which default to text must be followed by U+fe0f
|
||||
"(?:[©®\u2122\u265f]\ufe0f)|" +
|
||||
|
||||
// Variants that may be followed by U+fe0f but cannot be followed by U+fe0e
|
||||
"[\u203c\u2049\u2139\u2194-\u2199\u21a9\u21aa\u231a\u231b\u2328\u23cf\u23ed-\u23ef\u23f1" +
|
||||
"\u23f2\u23f8-\u23fa\u24c2\u25aa\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611" +
|
||||
"\u2614\u2615\u2618\u2620\u2622\u2623\u2626\u262a\u262e\u262f\u2638-\u263a\u2640\u2642" +
|
||||
"\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267b\u267f\u2692-\u2697\u2699\u269b\u269c" +
|
||||
"\u26a0\u26a1\u26a7\u26aa\u26ab\u26b0\u26b1\u26bd\u26be\u26c4\u26c5\u26c8\u26cf\u26d1\u26d3" +
|
||||
"\u26d4\u26e9\u26ea\u26f0-\u26f5\u26f8\u26fa\u26fd\u2702\u2708\u2709\u270f\u2712\u2714" +
|
||||
"\u2716\u271d\u2721\u2733\u2734\u2744\u2747\u2757\u2763\u2764\u27a1\u2934\u2935\u2b05-" +
|
||||
"\u2b07\u2b1b\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299\ud83c\udc04\ud83c\udd70\ud83c\udd71" +
|
||||
"\ud83c\udd7e\ud83c\udd7f\ud83c\ude02\ud83c\ude1a\ud83c\ude2f\ud83c\ude37\ud83c\udf21\ud83c" +
|
||||
"\udf24-\ud83c\udf2c\ud83c\udf36\ud83c\udf7d\ud83c\udf96\ud83c\udf97\ud83c\udf99-\ud83c" +
|
||||
"\udf9b\ud83c\udf9e\ud83c\udf9f\ud83c\udfcd\ud83c\udfce\ud83c\udfd4-\ud83c\udfdf\ud83c" +
|
||||
"\udff3\ud83c\udff5\ud83c\udff7\ud83d\udc3f\ud83d\udc41\ud83d\udcfd\ud83d\udd49\ud83d\udd4a" +
|
||||
"\ud83d\udd6f\ud83d\udd70\ud83d\udd73\ud83d\udd76-\ud83d\udd79\ud83d\udd87\ud83d\udd8a-" +
|
||||
"\ud83d\udd8d\ud83d\udda5\ud83d\udda8\ud83d\uddb1\ud83d\uddb2\ud83d\uddbc\ud83d\uddc2-" +
|
||||
"\ud83d\uddc4\ud83d\uddd1-\ud83d\uddd3\ud83d\udddc-\ud83d\uddde\ud83d\udde1\ud83d\udde3" +
|
||||
"\ud83d\udde8\ud83d\uddef\ud83d\uddf3\ud83d\uddfa\ud83d\udecb\ud83d\udecd-\ud83d\udecf" +
|
||||
"\ud83d\udee0-\ud83d\udee5\ud83d\udee9\ud83d\udef0\ud83d\udef3]" +
|
||||
"(?:\ufe0f|(?!\ufe0e))|" +
|
||||
|
||||
// Diversity Emoji followed by optional skin tone
|
||||
"(?:" +
|
||||
// Diversity variants that may be followed by U+fe0f but cannot be followed by U+fe0e
|
||||
"[\u261d\u26f7\u26f9\u270c\u270d\ud83c\udfcb\ud83c\udfcc\ud83d\udd74\ud83d\udd75\ud83d" +
|
||||
"\udd90]" +
|
||||
"(?:\ufe0f|(?!\ufe0e))|" +
|
||||
|
||||
// Diversity non-variants
|
||||
"[\u270a\u270b\ud83c\udf85\ud83c\udfc2-\ud83c\udfc4\ud83c\udfc7\ud83c\udfca\ud83d\udc42" +
|
||||
"\ud83d\udc43\ud83d\udc46-\ud83d\udc50\ud83d\udc66-\ud83d\udc69\ud83d\udc6e\ud83d\udc70-" +
|
||||
"\ud83d\udc78\ud83d\udc7c\ud83d\udc81-\ud83d\udc83\ud83d\udc85-\ud83d\udc87\ud83d\udcaa" +
|
||||
"\ud83d\udd7a\ud83d\udd95\ud83d\udd96\ud83d\ude45-\ud83d\ude47\ud83d\ude4b-\ud83d\ude4f" +
|
||||
"\ud83d\udea3\ud83d\udeb4-\ud83d\udeb6\ud83d\udec0\ud83d\udecc\ud83e\udd0f\ud83e\udd18-" +
|
||||
"\ud83e\udd1c\ud83e\udd1e\ud83e\udd1f\ud83e\udd26\ud83e\udd30-\ud83e\udd39\ud83e\udd3d" +
|
||||
"\ud83e\udd3e\ud83e\uddb5\ud83e\uddb6\ud83e\uddb8\ud83e\uddb9\ud83e\uddbb\ud83e\uddcd-" +
|
||||
"\ud83e\uddcf\ud83e\uddd1-\ud83e\udddd]" +
|
||||
")[\ud83c\udffb-\ud83c\udfff]?|" +
|
||||
|
||||
// Flags, Regional Symbols, and Normal Emoji
|
||||
"(?:\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f|" +
|
||||
"\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f|" +
|
||||
"\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f|" +
|
||||
"\ud83c\udde6[\ud83c\udde8-\ud83c\uddec\ud83c\uddee\ud83c\uddf1\ud83c\uddf2\ud83c\uddf4" +
|
||||
"\ud83c\uddf6-\ud83c\uddfa\ud83c\uddfc\ud83c\uddfd\ud83c\uddff]|\ud83c\udde7[\ud83c\udde6" +
|
||||
"\ud83c\udde7\ud83c\udde9-\ud83c\uddef\ud83c\uddf1-\ud83c\uddf4\ud83c\uddf6-\ud83c\uddf9" +
|
||||
"\ud83c\uddfb\ud83c\uddfc\ud83c\uddfe\ud83c\uddff]|\ud83c\udde8[\ud83c\udde6\ud83c\udde8" +
|
||||
"\ud83c\udde9\ud83c\uddeb-\ud83c\uddee\ud83c\uddf0-\ud83c\uddf5\ud83c\uddf7\ud83c\uddfa-" +
|
||||
"\ud83c\uddff]|\ud83c\udde9[\ud83c\uddea\ud83c\uddec\ud83c\uddef\ud83c\uddf0\ud83c\uddf2" +
|
||||
"\ud83c\uddf4\ud83c\uddff]|\ud83c\uddea[\ud83c\udde6\ud83c\udde8\ud83c\uddea\ud83c\uddec" +
|
||||
"\ud83c\udded\ud83c\uddf7-\ud83c\uddfa]|\ud83c\uddeb[\ud83c\uddee-\ud83c\uddf0\ud83c\uddf2" +
|
||||
"\ud83c\uddf4\ud83c\uddf7]|\ud83c\uddec[\ud83c\udde6\ud83c\udde7\ud83c\udde9-\ud83c\uddee" +
|
||||
"\ud83c\uddf1-\ud83c\uddf3\ud83c\uddf5-\ud83c\uddfa\ud83c\uddfc\ud83c\uddfe]|\ud83c\udded[" +
|
||||
"\ud83c\uddf0\ud83c\uddf2\ud83c\uddf3\ud83c\uddf7\ud83c\uddf9\ud83c\uddfa]|\ud83c\uddee[" +
|
||||
"\ud83c\udde8-\ud83c\uddea\ud83c\uddf1-\ud83c\uddf4\ud83c\uddf6-\ud83c\uddf9]|\ud83c\uddef[" +
|
||||
"\ud83c\uddea\ud83c\uddf2\ud83c\uddf4\ud83c\uddf5]|\ud83c\uddf0[\ud83c\uddea\ud83c\uddec-" +
|
||||
"\ud83c\uddee\ud83c\uddf2\ud83c\uddf3\ud83c\uddf5\ud83c\uddf7\ud83c\uddfc\ud83c\uddfe\ud83c" +
|
||||
"\uddff]|\ud83c\uddf1[\ud83c\udde6-\ud83c\udde8\ud83c\uddee\ud83c\uddf0\ud83c\uddf7-\ud83c" +
|
||||
"\uddfb\ud83c\uddfe]|\ud83c\uddf2[\ud83c\udde6\ud83c\udde8-\ud83c\udded\ud83c\uddf0-\ud83c" +
|
||||
"\uddff]|\ud83c\uddf3[\ud83c\udde6\ud83c\udde8\ud83c\uddea-\ud83c\uddec\ud83c\uddee\ud83c" +
|
||||
"\uddf1\ud83c\uddf4\ud83c\uddf5\ud83c\uddf7\ud83c\uddfa\ud83c\uddff]|\ud83c\uddf4\ud83c" +
|
||||
"\uddf2|\ud83c\uddf5[\ud83c\udde6\ud83c\uddea-\ud83c\udded\ud83c\uddf0-\ud83c\uddf3\ud83c" +
|
||||
"\uddf7-\ud83c\uddf9\ud83c\uddfc\ud83c\uddfe]|\ud83c\uddf6\ud83c\udde6|\ud83c\uddf7[\ud83c" +
|
||||
"\uddea\ud83c\uddf4\ud83c\uddf8\ud83c\uddfa\ud83c\uddfc]|\ud83c\uddf8[\ud83c\udde6-\ud83c" +
|
||||
"\uddea\ud83c\uddec-\ud83c\uddf4\ud83c\uddf7-\ud83c\uddf9\ud83c\uddfb\ud83c\uddfd-\ud83c" +
|
||||
"\uddff]|\ud83c\uddf9[\ud83c\udde6\ud83c\udde8\ud83c\udde9\ud83c\uddeb-\ud83c\udded\ud83c" +
|
||||
"\uddef-\ud83c\uddf4\ud83c\uddf7\ud83c\uddf9\ud83c\uddfb\ud83c\uddfc\ud83c\uddff]|\ud83c" +
|
||||
"\uddfa[\ud83c\udde6\ud83c\uddec\ud83c\uddf2\ud83c\uddf3\ud83c\uddf8\ud83c\uddfe\ud83c" +
|
||||
"\uddff]|\ud83c\uddfb[\ud83c\udde6\ud83c\udde8\ud83c\uddea\ud83c\uddec\ud83c\uddee\ud83c" +
|
||||
"\uddf3\ud83c\uddfa]|\ud83c\uddfc[\ud83c\uddeb\ud83c\uddf8]|\ud83c\uddfd\ud83c\uddf0|\ud83c" +
|
||||
"\uddfe[\ud83c\uddea\ud83c\uddf9]|\ud83c\uddff[\ud83c\udde6\ud83c\uddf2\ud83c\uddfc]|[" +
|
||||
"\u23e9-\u23ec\u23f0\u23f3\u267e\u26ce\u2705\u2728\u274c\u274e\u2753-\u2755\u2795-\u2797" +
|
||||
"\u27b0\u27bf\ue50a\ud83c\udccf\ud83c\udd8e\ud83c\udd91-\ud83c\udd9a\ud83c\udde6-\ud83c" +
|
||||
"\uddff\ud83c\ude01\ud83c\ude32-\ud83c\ude36\ud83c\ude38-\ud83c\ude3a\ud83c\ude50\ud83c" +
|
||||
"\ude51\ud83c\udf00-\ud83c\udf20\ud83c\udf2d-\ud83c\udf35\ud83c\udf37-\ud83c\udf7c\ud83c" +
|
||||
"\udf7e-\ud83c\udf84\ud83c\udf86-\ud83c\udf93\ud83c\udfa0-\ud83c\udfc1\ud83c\udfc5\ud83c" +
|
||||
"\udfc6\ud83c\udfc8\ud83c\udfc9\ud83c\udfcf-\ud83c\udfd3\ud83c\udfe0-\ud83c\udff0\ud83c" +
|
||||
"\udff4\ud83c\udff8-\ud83d\udc3e\ud83d\udc40\ud83d\udc44\ud83d\udc45\ud83d\udc51-\ud83d" +
|
||||
"\udc65\ud83d\udc6a\ud83d\udc6f\ud83d\udc79-\ud83d\udc7b\ud83d\udc7d-\ud83d\udc80\ud83d" +
|
||||
"\udc84\ud83d\udc88-\ud83d\udca9\ud83d\udcab-\ud83d\udcfc\ud83d\udcff-\ud83d\udd3d\ud83d" +
|
||||
"\udd4b-\ud83d\udd4e\ud83d\udd50-\ud83d\udd67\ud83d\udda4\ud83d\uddfb-\ud83d\ude44\ud83d" +
|
||||
"\ude48-\ud83d\ude4a\ud83d\ude80-\ud83d\udea2\ud83d\udea4-\ud83d\udeb3\ud83d\udeb7-\ud83d" +
|
||||
"\udebf\ud83d\udec1-\ud83d\udec5\ud83d\uded0-\ud83d\uded2\ud83d\uded5\ud83d\udeeb\ud83d" +
|
||||
"\udeec\ud83d\udef4-\ud83d\udefa\ud83d\udfe0-\ud83d\udfeb\ud83e\udd0d\ud83e\udd0e\ud83e" +
|
||||
"\udd10-\ud83e\udd17\ud83e\udd1d\ud83e\udd20-\ud83e\udd25\ud83e\udd27-\ud83e\udd2f\ud83e" +
|
||||
"\udd3a\ud83e\udd3c\ud83e\udd3f-\ud83e\udd45\ud83e\udd47-\ud83e\udd71\ud83e\udd73-\ud83e" +
|
||||
"\udd76\ud83e\udd7a-\ud83e\udda2\ud83e\udda5-\ud83e\uddaa\ud83e\uddae-\ud83e\uddb4\ud83e" +
|
||||
"\uddb7\ud83e\uddba\ud83e\uddbc-\ud83e\uddca\ud83e\uddd0\ud83e\uddde-\ud83e\uddff\ud83e" +
|
||||
"\ude70-\ud83e\ude73\ud83e\ude78-\ud83e\ude7a\ud83e\ude80-\ud83e\ude82\ud83e\ude90-\ud83e" +
|
||||
"\ude95])" + "|" +
|
||||
|
||||
// Stray VS16s can adversely affect preceeding non-emoji characters
|
||||
"\ufe0f";
|
||||
|
||||
public static final Pattern VALID_EMOJI_PATTERN;
|
||||
|
||||
static {
|
||||
synchronized (TwitterTextEmojiRegex.class) {
|
||||
VALID_EMOJI_PATTERN = Pattern.compile(EMOJI);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ public class MainActivity extends FragmentStackActivity{
|
|||
if(AccountSessionManager.getInstance().getLoggedInAccounts().isEmpty()){
|
||||
showFragmentClearingBackStack(new SplashFragment());
|
||||
}else{
|
||||
AccountSessionManager.getInstance().maybeUpdateLocalInfo();
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", AccountSessionManager.getInstance().getLastActiveAccountID());
|
||||
HomeFragment fragment=new HomeFragment();
|
||||
|
|
|
@ -163,4 +163,8 @@ public class MastodonAPIController{
|
|||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public static void runInBackground(Runnable action){
|
||||
thread.postRunnable(action, 0);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
package org.joinmastodon.android.api.requests;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetCustomEmojis extends MastodonAPIRequest<List<Emoji>>{
|
||||
public GetCustomEmojis(){
|
||||
super(HttpMethod.GET, "/custom_emojis", new TypeToken<>(){});
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ public class AccountSession{
|
|||
public String domain;
|
||||
public int tootCharLimit;
|
||||
public Application app;
|
||||
public long infoLastUpdated;
|
||||
private transient MastodonAPIController apiController;
|
||||
|
||||
AccountSession(Token token, Account self, Application app, String domain, int tootCharLimit){
|
||||
|
@ -19,6 +20,7 @@ public class AccountSession{
|
|||
this.domain=domain;
|
||||
this.app=app;
|
||||
this.tootCharLimit=tootCharLimit;
|
||||
infoLastUpdated=System.currentTimeMillis();
|
||||
}
|
||||
|
||||
AccountSession(){}
|
||||
|
|
|
@ -2,21 +2,22 @@ package org.joinmastodon.android.api.session;
|
|||
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.OAuthActivity;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.requests.GetCustomEmojis;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
|
||||
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Application;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.EmojiCategory;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
|
||||
|
@ -28,8 +29,13 @@ import java.io.InputStreamReader;
|
|||
import java.io.OutputStreamWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
@ -46,11 +52,14 @@ public class AccountSessionManager{
|
|||
private static final AccountSessionManager instance=new AccountSessionManager();
|
||||
|
||||
private HashMap<String, AccountSession> sessions=new HashMap<>();
|
||||
private HashMap<String, List<EmojiCategory>> customEmojis=new HashMap<>();
|
||||
private HashMap<String, Long> customEmojisLastUpdated=new HashMap<>();
|
||||
private MastodonAPIController unauthenticatedApiController=new MastodonAPIController(null);
|
||||
private Instance authenticatingInstance;
|
||||
private Application authenticatingApp;
|
||||
private String lastActiveAccountID;
|
||||
private SharedPreferences prefs;
|
||||
private boolean loadedCustomEmojis;
|
||||
|
||||
public static AccountSessionManager getInstance(){
|
||||
return instance;
|
||||
|
@ -61,15 +70,18 @@ public class AccountSessionManager{
|
|||
File file=new File(MastodonApp.context.getFilesDir(), "accounts.json");
|
||||
if(!file.exists())
|
||||
return;
|
||||
HashSet<String> domains=new HashSet<>();
|
||||
try(FileInputStream in=new FileInputStream(file)){
|
||||
SessionsStorageWrapper w=MastodonAPIController.gson.fromJson(new InputStreamReader(in, StandardCharsets.UTF_8), SessionsStorageWrapper.class);
|
||||
for(AccountSession session:w.accounts){
|
||||
domains.add(session.domain.toLowerCase());
|
||||
sessions.put(session.getID(), session);
|
||||
}
|
||||
}catch(IOException x){
|
||||
}catch(IOException|JsonParseException x){
|
||||
Log.e(TAG, "Error loading accounts", x);
|
||||
}
|
||||
lastActiveAccountID=prefs.getString("lastActiveAccount", null);
|
||||
MastodonAPIController.runInBackground(()->readCustomEmojis(domains));
|
||||
}
|
||||
|
||||
public void addAccount(Instance instance, Token token, Account self, Application app){
|
||||
|
@ -129,6 +141,10 @@ public class AccountSessionManager{
|
|||
lastActiveAccountID=getLoggedInAccounts().get(0).getID();
|
||||
}
|
||||
writeAccountsFile();
|
||||
String domain=session.domain.toLowerCase();
|
||||
if(sessions.isEmpty() || !sessions.values().stream().map(s->s.domain.toLowerCase()).collect(Collectors.toSet()).contains(domain)){
|
||||
getCustomEmojisFile(domain).delete();
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
@ -181,7 +197,122 @@ public class AccountSessionManager{
|
|||
return authenticatingApp;
|
||||
}
|
||||
|
||||
public void maybeUpdateLocalInfo(){
|
||||
long now=System.currentTimeMillis();
|
||||
HashSet<String> domains=new HashSet<>();
|
||||
for(AccountSession session:sessions.values()){
|
||||
domains.add(session.domain.toLowerCase());
|
||||
if(now-session.infoLastUpdated>24L*3600_000L){
|
||||
updateSessionLocalInfo(session);
|
||||
}
|
||||
}
|
||||
if(loadedCustomEmojis){
|
||||
maybeUpdateCustomEmojis(domains);
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeUpdateCustomEmojis(Set<String> domains){
|
||||
long now=System.currentTimeMillis();
|
||||
for(String domain:domains){
|
||||
Long lastUpdated=customEmojisLastUpdated.get(domain);
|
||||
if(lastUpdated==null || now-lastUpdated>24L*3600_000L){
|
||||
updateCustomEmojis(domain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSessionLocalInfo(AccountSession session){
|
||||
new GetOwnAccount()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Account result){
|
||||
session.self=result;
|
||||
session.infoLastUpdated=System.currentTimeMillis();
|
||||
writeAccountsFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
|
||||
}
|
||||
})
|
||||
.exec(session.getID());
|
||||
}
|
||||
|
||||
private void updateCustomEmojis(String domain){
|
||||
new GetCustomEmojis()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Emoji> result){
|
||||
CustomEmojisStorageWrapper emojis=new CustomEmojisStorageWrapper();
|
||||
emojis.lastUpdated=System.currentTimeMillis();
|
||||
emojis.emojis=result;
|
||||
customEmojis.put(domain, groupCustomEmojis(emojis));
|
||||
customEmojisLastUpdated.put(domain, emojis.lastUpdated);
|
||||
MastodonAPIController.runInBackground(()->writeCustomEmojisFile(emojis, domain));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
|
||||
}
|
||||
})
|
||||
.execNoAuth(domain);
|
||||
}
|
||||
|
||||
private File getCustomEmojisFile(String domain){
|
||||
return new File(MastodonApp.context.getFilesDir(), "emojis_"+domain.replace('.', '_')+".json");
|
||||
}
|
||||
|
||||
private void writeCustomEmojisFile(CustomEmojisStorageWrapper emojis, String domain){
|
||||
try(FileOutputStream out=new FileOutputStream(getCustomEmojisFile(domain))){
|
||||
OutputStreamWriter writer=new OutputStreamWriter(out, StandardCharsets.UTF_8);
|
||||
MastodonAPIController.gson.toJson(emojis, writer);
|
||||
writer.flush();
|
||||
}catch(IOException x){
|
||||
Log.w(TAG, "Error writing emojis file for "+domain, x);
|
||||
}
|
||||
}
|
||||
|
||||
private void readCustomEmojis(Set<String> domains){
|
||||
for(String domain:domains){
|
||||
try(FileInputStream in=new FileInputStream(getCustomEmojisFile(domain))){
|
||||
InputStreamReader reader=new InputStreamReader(in, StandardCharsets.UTF_8);
|
||||
CustomEmojisStorageWrapper emojis=MastodonAPIController.gson.fromJson(reader, CustomEmojisStorageWrapper.class);
|
||||
customEmojis.put(domain, groupCustomEmojis(emojis));
|
||||
customEmojisLastUpdated.put(domain, emojis.lastUpdated);
|
||||
}catch(IOException|JsonParseException x){
|
||||
Log.w(TAG, "Error reading emojis file for "+domain, x);
|
||||
}
|
||||
}
|
||||
if(!loadedCustomEmojis){
|
||||
loadedCustomEmojis=true;
|
||||
maybeUpdateCustomEmojis(domains);
|
||||
}
|
||||
}
|
||||
|
||||
private List<EmojiCategory> groupCustomEmojis(CustomEmojisStorageWrapper emojis){
|
||||
return emojis.emojis.stream()
|
||||
.filter(e->e.visibleInPicker)
|
||||
.collect(Collectors.groupingBy(e->e.category==null ? "" : e.category))
|
||||
.entrySet()
|
||||
.stream()
|
||||
.map(e->new EmojiCategory(e.getKey(), e.getValue()))
|
||||
.sorted(Comparator.comparing(c->c.title))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<EmojiCategory> getCustomEmojis(String domain){
|
||||
List<EmojiCategory> r=customEmojis.get(domain.toLowerCase());
|
||||
return r==null ? Collections.emptyList() : r;
|
||||
}
|
||||
|
||||
private static class SessionsStorageWrapper{
|
||||
public List<AccountSession> accounts;
|
||||
}
|
||||
|
||||
private static class CustomEmojisStorageWrapper{
|
||||
public List<Emoji> emojis;
|
||||
public long lastUpdated;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Outline;
|
||||
import android.icu.text.BreakIterator;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
|
@ -14,10 +17,12 @@ import android.view.ViewGroup;
|
|||
import android.view.ViewOutlineProvider;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.twitter.twittertext.Regex;
|
||||
import com.twitter.twittertext.TwitterTextEmojiRegex;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
|
@ -26,9 +31,14 @@ import org.joinmastodon.android.api.session.AccountSession;
|
|||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.EmojiCategory;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.CustomEmojiPopupKeyboard;
|
||||
import org.joinmastodon.android.ui.PopupKeyboard;
|
||||
import org.joinmastodon.android.ui.views.SizeListenerLinearLayout;
|
||||
|
||||
import java.text.BreakIterator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
@ -59,8 +69,10 @@ public class ComposeFragment extends ToolbarFragment{
|
|||
")" +
|
||||
")";
|
||||
private static final Pattern URL_PATTERN=Pattern.compile(VALID_URL_PATTERN_STRING, Pattern.CASE_INSENSITIVE);
|
||||
@SuppressLint("NewApi") // this class actually exists on 6.0
|
||||
private final BreakIterator breakIterator=BreakIterator.getCharacterInstance();
|
||||
|
||||
private SizeListenerLinearLayout contentView;
|
||||
private TextView selfName, selfUsername;
|
||||
private ImageView selfAvatar;
|
||||
private Account self;
|
||||
|
@ -72,6 +84,10 @@ public class ComposeFragment extends ToolbarFragment{
|
|||
private int charCount, charLimit;
|
||||
|
||||
private MenuItem publishButton;
|
||||
private ImageButton emojiBtn;
|
||||
|
||||
private List<EmojiCategory> customEmojis;
|
||||
private CustomEmojiPopupKeyboard emojiKeyboard;
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
|
@ -84,6 +100,9 @@ public class ComposeFragment extends ToolbarFragment{
|
|||
charLimit=500;
|
||||
self=session.self;
|
||||
instanceDomain=session.domain;
|
||||
customEmojis=AccountSessionManager.getInstance().getCustomEmojis(instanceDomain);
|
||||
emojiKeyboard=new CustomEmojiPopupKeyboard(activity, customEmojis, instanceDomain);
|
||||
emojiKeyboard.setListener(this::onCustomEmojiClick);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -108,6 +127,18 @@ public class ComposeFragment extends ToolbarFragment{
|
|||
selfAvatar.setOutlineProvider(roundCornersOutline);
|
||||
selfAvatar.setClipToOutline(true);
|
||||
|
||||
emojiBtn=view.findViewById(R.id.btn_emoji);
|
||||
emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText));
|
||||
emojiKeyboard.setOnIconChangedListener(new PopupKeyboard.OnIconChangeListener(){
|
||||
@Override
|
||||
public void onIconChanged(int icon){
|
||||
emojiBtn.setSelected(icon!=PopupKeyboard.ICON_HIDDEN);
|
||||
}
|
||||
});
|
||||
|
||||
contentView=(SizeListenerLinearLayout) view;
|
||||
contentView.addView(emojiKeyboard.getView());
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
|
@ -119,9 +150,10 @@ public class ComposeFragment extends ToolbarFragment{
|
|||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
contentView.setSizeListener(emojiKeyboard::onContentViewSizeChanged);
|
||||
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
|
||||
view.postDelayed(()->{
|
||||
mainEditText.requestFocus();
|
||||
view.postDelayed(()->{
|
||||
imm.showSoftInput(mainEditText, 0);
|
||||
}, 100);
|
||||
|
||||
|
@ -173,10 +205,21 @@ public class ComposeFragment extends ToolbarFragment{
|
|||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig){
|
||||
super.onConfigurationChanged(newConfig);
|
||||
emojiKeyboard.onConfigurationChanged();
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
private void updateCharCounter(CharSequence text){
|
||||
String countableText=MENTION_PATTERN.matcher(URL_PATTERN.matcher(text).replaceAll("$2xxxxxxxxxxxxxxxxxxxxxxx")).replaceAll("$1@$3");
|
||||
breakIterator.setText(countableText);
|
||||
String countableText=TwitterTextEmojiRegex.VALID_EMOJI_PATTERN.matcher(
|
||||
MENTION_PATTERN.matcher(
|
||||
URL_PATTERN.matcher(text).replaceAll("$2xxxxxxxxxxxxxxxxxxxxxxx")
|
||||
).replaceAll("$1@$3")
|
||||
).replaceAll("x");
|
||||
charCount=0;
|
||||
breakIterator.setText(countableText);
|
||||
while(breakIterator.next()!=BreakIterator.DONE){
|
||||
charCount++;
|
||||
}
|
||||
|
@ -188,4 +231,8 @@ public class ComposeFragment extends ToolbarFragment{
|
|||
private void updatePublishButtonState(){
|
||||
publishButton.setEnabled(charCount>0 && charCount<=charLimit);
|
||||
}
|
||||
|
||||
private void onCustomEmojiClick(Emoji emoji){
|
||||
mainEditText.getText().replace(mainEditText.getSelectionStart(), mainEditText.getSelectionEnd(), ':'+emoji.shortcode+':');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package org.joinmastodon.android.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class EmojiCategory{
|
||||
public String title;
|
||||
public List<Emoji> emojis;
|
||||
|
||||
public EmojiCategory(String title, List<Emoji> emojis){
|
||||
this.title=title;
|
||||
this.emojis=emojis;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
package org.joinmastodon.android.ui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.EmojiCategory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.ListImageLoaderWrapper;
|
||||
import me.grishka.appkit.imageloader.RecyclerViewDelegate;
|
||||
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.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class CustomEmojiPopupKeyboard extends PopupKeyboard{
|
||||
private List<EmojiCategory> emojis;
|
||||
private UsableRecyclerView list;
|
||||
private ListImageLoaderWrapper imgLoader;
|
||||
private MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
private String domain;
|
||||
private int gridGap;
|
||||
private int spanCount=6;
|
||||
private Consumer<Emoji> listener;
|
||||
|
||||
public CustomEmojiPopupKeyboard(Activity activity, List<EmojiCategory> emojis, String domain){
|
||||
super(activity);
|
||||
this.emojis=emojis;
|
||||
this.domain=domain;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View onCreateView(){
|
||||
GridLayoutManager lm=new GridLayoutManager(activity, spanCount);
|
||||
list=new UsableRecyclerView(activity){
|
||||
@Override
|
||||
protected void onMeasure(int widthSpec, int heightSpec){
|
||||
// it's important to do this in onMeasure so the child views will be measured with correct paddings already set
|
||||
spanCount=Math.round(MeasureSpec.getSize(widthSpec)/(float)V.dp(44+20));
|
||||
lm.setSpanCount(spanCount);
|
||||
int pad=V.dp(16);
|
||||
gridGap=(MeasureSpec.getSize(widthSpec)-pad*2-V.dp(44)*spanCount)/(spanCount-1);
|
||||
setPadding(pad, 0, pad-gridGap, 0);
|
||||
invalidateItemDecorations();
|
||||
super.onMeasure(widthSpec, heightSpec);
|
||||
}
|
||||
};
|
||||
lm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup(){
|
||||
@Override
|
||||
public int getSpanSize(int position){
|
||||
if(adapter.getItemViewType(position)==0)
|
||||
return lm.getSpanCount();
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
list.setLayoutManager(lm);
|
||||
imgLoader=new ListImageLoaderWrapper(activity, list, new RecyclerViewDelegate(list), null);
|
||||
|
||||
for(EmojiCategory category:emojis)
|
||||
adapter.addAdapter(new SingleCategoryAdapter(category));
|
||||
list.setAdapter(adapter);
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
@Override
|
||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
outRect.right=gridGap;
|
||||
if(view instanceof TextView){ // section header
|
||||
if(parent.getChildAdapterPosition(view)>0)
|
||||
outRect.top=-gridGap; // negate the margin added by the emojis above
|
||||
}else{
|
||||
outRect.bottom=gridGap;
|
||||
}
|
||||
}
|
||||
});
|
||||
list.setBackgroundResource(R.color.gray_100);
|
||||
list.setSelector(null);
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public void setListener(Consumer<Emoji> listener){
|
||||
this.listener=listener;
|
||||
}
|
||||
|
||||
private class SingleCategoryAdapter extends UsableRecyclerView.Adapter<RecyclerView.ViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
private final EmojiCategory category;
|
||||
private final List<ImageLoaderRequest> requests;
|
||||
|
||||
public SingleCategoryAdapter(EmojiCategory category){
|
||||
super(imgLoader);
|
||||
this.category=category;
|
||||
requests=category.emojis.stream().map(e->new UrlImageLoaderRequest(e.url, V.dp(44), V.dp(44))).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return viewType==0 ? new SectionHeaderViewHolder() : new EmojiViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position){
|
||||
if(holder instanceof EmojiViewHolder){
|
||||
((EmojiViewHolder) holder).bind(category.emojis.get(position-1));
|
||||
((EmojiViewHolder) holder).positionWithinCategory=position-1;
|
||||
}else if(holder instanceof SectionHeaderViewHolder){
|
||||
((SectionHeaderViewHolder) holder).bind(TextUtils.isEmpty(category.title) ? domain : category.title);
|
||||
}
|
||||
super.onBindViewHolder(holder, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return category.emojis.size()+1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position){
|
||||
return position==0 ? 0 : 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImageCountForItem(int position){
|
||||
return position>0 ? 1 : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int position, int image){
|
||||
return requests.get(position-1);
|
||||
}
|
||||
}
|
||||
|
||||
private class SectionHeaderViewHolder extends BindableViewHolder<String>{
|
||||
public SectionHeaderViewHolder(){
|
||||
super(activity, R.layout.item_emoji_section, list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(String item){
|
||||
((TextView)itemView).setText(item);
|
||||
}
|
||||
}
|
||||
|
||||
private class EmojiViewHolder extends BindableViewHolder<Emoji> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
|
||||
public int positionWithinCategory;
|
||||
public EmojiViewHolder(){
|
||||
super(new ImageView(activity));
|
||||
ImageView img=(ImageView) itemView;
|
||||
img.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(44)));
|
||||
img.setScaleType(ImageView.ScaleType.FIT_CENTER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(Emoji item){
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable image){
|
||||
((ImageView)itemView).setImageDrawable(image);
|
||||
if(image instanceof Animatable)
|
||||
((Animatable) image).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
((ImageView)itemView).setImageDrawable(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
listener.accept(item);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
package org.joinmastodon.android.ui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.Window;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
/**
|
||||
* Created by grishka on 17.08.15.
|
||||
*/
|
||||
public abstract class PopupKeyboard{
|
||||
|
||||
protected View keyboardPopupView;
|
||||
protected Activity activity;
|
||||
private int initialHeight;
|
||||
private int prevWidth;
|
||||
private int keyboardHeight;
|
||||
private boolean needShowOnHide=false;
|
||||
private boolean keyboardWasVisible=false;
|
||||
private OnIconChangeListener iconListener;
|
||||
|
||||
public static final int ICON_HIDDEN=0;
|
||||
public static final int ICON_ARROW=1;
|
||||
public static final int ICON_KEYBOARD=2;
|
||||
|
||||
public PopupKeyboard(Activity activity){
|
||||
this.activity=activity;
|
||||
}
|
||||
|
||||
protected abstract View onCreateView();
|
||||
|
||||
private void ensureView(){
|
||||
if(keyboardPopupView==null){
|
||||
keyboardPopupView=onCreateView();
|
||||
keyboardPopupView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
public View getView(){
|
||||
ensureView();
|
||||
return keyboardPopupView;
|
||||
}
|
||||
|
||||
public boolean isVisible(){
|
||||
ensureView();
|
||||
return keyboardPopupView.getVisibility()==View.VISIBLE;
|
||||
}
|
||||
|
||||
public void toggleKeyboardPopup(View textField){
|
||||
ensureView();
|
||||
if(keyboardPopupView.getVisibility()==View.VISIBLE){
|
||||
if(keyboardWasVisible){
|
||||
keyboardWasVisible=false;
|
||||
InputMethodManager imm=(InputMethodManager)activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.showSoftInput(textField, 0);
|
||||
}else{
|
||||
keyboardPopupView.setVisibility(View.GONE);
|
||||
}
|
||||
if(iconListener!=null)
|
||||
iconListener.onIconChanged(ICON_HIDDEN);
|
||||
return;
|
||||
}
|
||||
if(keyboardHeight>0){
|
||||
needShowOnHide=true;
|
||||
keyboardWasVisible=true;
|
||||
InputMethodManager imm=(InputMethodManager)activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(getWindow().getDecorView().getWindowToken(), 0);
|
||||
if(iconListener!=null)
|
||||
iconListener.onIconChanged(ICON_KEYBOARD);
|
||||
}else{
|
||||
doShowKeyboardPopup();
|
||||
if(iconListener!=null)
|
||||
iconListener.onIconChanged(ICON_ARROW);
|
||||
}
|
||||
}
|
||||
|
||||
protected Window getWindow(){
|
||||
return activity.getWindow();
|
||||
}
|
||||
|
||||
public void setOnIconChangedListener(OnIconChangeListener l){
|
||||
iconListener=l;
|
||||
}
|
||||
|
||||
public void onContentViewSizeChanged(int w, int h, int oldw, int oldh){
|
||||
if(oldw==0 || w!=prevWidth){
|
||||
initialHeight=h;
|
||||
prevWidth=w;
|
||||
onWidthChanged(w);
|
||||
}
|
||||
if(h>initialHeight){
|
||||
initialHeight=h;
|
||||
}
|
||||
if(initialHeight!=0 && w==oldw){
|
||||
keyboardHeight=initialHeight-h;
|
||||
if(keyboardHeight!=0){
|
||||
DisplayMetrics dm=activity.getResources().getDisplayMetrics();
|
||||
activity.getSharedPreferences("emoji", Context.MODE_PRIVATE).edit().putInt("kb_size"+dm.widthPixels+"_"+dm.heightPixels, keyboardHeight).commit();
|
||||
}
|
||||
if(needShowOnHide && keyboardHeight==0){
|
||||
((View)keyboardPopupView.getParent()).getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
((View)keyboardPopupView.getParent()).getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
doShowKeyboardPopup();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
needShowOnHide=false;
|
||||
}
|
||||
if(keyboardHeight>0 && keyboardPopupView.getVisibility()==View.VISIBLE){
|
||||
if(iconListener!=null)
|
||||
iconListener.onIconChanged(ICON_HIDDEN);
|
||||
((View)keyboardPopupView.getParent()).getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
((View)keyboardPopupView.getParent()).getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
keyboardPopupView.setVisibility(View.GONE);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void hide(){
|
||||
ensureView();
|
||||
if(keyboardPopupView.getVisibility()==View.VISIBLE){
|
||||
keyboardPopupView.setVisibility(View.GONE);
|
||||
keyboardWasVisible=false;
|
||||
if(iconListener!=null)
|
||||
iconListener.onIconChanged(ICON_HIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
public void onConfigurationChanged(){
|
||||
|
||||
}
|
||||
|
||||
protected void onWidthChanged(int w){
|
||||
|
||||
}
|
||||
|
||||
protected boolean needWrapContent(){
|
||||
return false;
|
||||
}
|
||||
|
||||
private void doShowKeyboardPopup(){
|
||||
ensureView();
|
||||
DisplayMetrics dm=activity.getResources().getDisplayMetrics();
|
||||
int height=activity.getSharedPreferences("emoji", Context.MODE_PRIVATE).getInt("kb_size"+dm.widthPixels+"_"+dm.heightPixels, V.dp(200));
|
||||
if(needWrapContent()){
|
||||
keyboardPopupView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.AT_MOST | height);
|
||||
height=keyboardPopupView.getMeasuredHeight();
|
||||
}
|
||||
keyboardPopupView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height));
|
||||
keyboardPopupView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
public interface OnIconChangeListener{
|
||||
public void onIconChanged(int icon);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class SizeListenerLinearLayout extends LinearLayout{
|
||||
private OnSizeChangedListener sizeListener;
|
||||
|
||||
public SizeListenerLinearLayout(Context context){
|
||||
super(context);
|
||||
}
|
||||
|
||||
public SizeListenerLinearLayout(Context context, @Nullable AttributeSet attrs){
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public SizeListenerLinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr){
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh){
|
||||
if(sizeListener!=null)
|
||||
sizeListener.onSizeChanged(w, h, oldw, oldh);
|
||||
}
|
||||
|
||||
public void setSizeListener(OnSizeChangedListener sizeListener){
|
||||
this.sizeListener=sizeListener;
|
||||
}
|
||||
//
|
||||
// @Override
|
||||
// public View findFocus(){
|
||||
// View v=super.findFocus();
|
||||
// Log.w("11", "findFocus() "+v);
|
||||
// return v;
|
||||
// }
|
||||
|
||||
@FunctionalInterface
|
||||
public interface OnSizeChangedListener{
|
||||
void onSizeChanged(int w, int h, int oldw, int oldh);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package org.joinmastodon.android.utils;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* from https://github.com/Richienb/char-regex/blob/master/index.js
|
||||
*/
|
||||
public class CharRegex{
|
||||
// Used to compose unicode character classes.
|
||||
private static final String astralRange = "\\ud800-\\udfff";
|
||||
private static final String comboMarksRange = "\\u0300-\\u036f";
|
||||
private static final String comboHalfMarksRange = "\\ufe20-\\ufe2f";
|
||||
private static final String comboSymbolsRange = "\\u20d0-\\u20ff";
|
||||
private static final String comboMarksExtendedRange = "\\u1ab0-\\u1aff";
|
||||
private static final String comboMarksSupplementRange = "\\u1dc0-\\u1dff";
|
||||
private static final String comboRange = comboMarksRange + comboHalfMarksRange + comboSymbolsRange + comboMarksExtendedRange + comboMarksSupplementRange;
|
||||
private static final String varRange = "\\ufe0e\\ufe0f";
|
||||
|
||||
|
||||
// Used to compose unicode capture groups.
|
||||
private static final String astral = "["+astralRange+"]";
|
||||
private static final String combo = "["+comboRange+"]";
|
||||
private static final String fitz = "\\ud83c[\\udffb-\\udfff]";
|
||||
private static final String modifier = "(?:"+combo+"|"+fitz+")";
|
||||
private static final String nonAstral = "[^"+astralRange+"]";
|
||||
private static final String regional = "(?:\\ud83c[\\udde6-\\uddff]){2}";
|
||||
private static final String surrogatePair = "[\\ud800-\\udbff][\\udc00-\\udfff]";
|
||||
private static final String zeroWidthJoiner = "\\u200d";
|
||||
private static final String blackFlag = "(?:\\ud83c\\udff4\\udb40\\udc67\\udb40\\udc62\\udb40(?:\\udc65|\\udc73|\\udc77)\\udb40(?:\\udc6e|\\udc63|\\udc6c)\\udb40(?:\\udc67|\\udc74|\\udc73)\\udb40\\udc7f)";
|
||||
|
||||
// Used to compose unicode regexes.
|
||||
private static final String optModifier = modifier+"?";
|
||||
private static final String optVar = "["+varRange+"]?";
|
||||
private static final String optJoin = "(?:"+zeroWidthJoiner+"(?:"+nonAstral+"|"+regional+"|"+surrogatePair+")"+optVar + optModifier+")*";
|
||||
private static final String seq = optVar + optModifier + optJoin;
|
||||
private static final String nonAstralCombo = nonAstral+combo+"?";
|
||||
private static final String symbol = "(?:"+blackFlag+"|"+nonAstralCombo+"|"+combo+"|"+regional+"|"+surrogatePair+"|"+astral+")";
|
||||
|
||||
public static final Pattern REGEX=Pattern.compile(fitz+"(?="+fitz+")|"+symbol + seq);
|
||||
// public static final Pattern REGEX=Pattern.compile("\\ud83c[\\udffb-\\udfff](?=\\ud83c[\\udffb-\\udfff])|(?:(?:\\ud83c\\udff4\\udb40\\udc67\\udb40\\udc62\\udb40(?:\\udc65|\\udc73|\\udc77)\\udb40(?:\\udc6e|\\udc63|\\udc6c)\\udb40(?:\\udc67|\\udc74|\\udc73)\\udb40\\udc7f)|[^\\ud800-\\udfff][\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff\\u1ab0-\\u1aff\\u1dc0-\\u1dff]?|[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff\\u1ab0-\\u1aff\\u1dc0-\\u1dff]|(?:\\ud83c[\\udde6-\\uddff]){2}|[\\ud800-\\udbff][\\udc00-\\udfff]|[\\ud800-\\udfff])[\\ufe0e\\ufe0f]?(?:[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff\\u1ab0-\\u1aff\\u1dc0-\\u1dff]|\\ud83c[\\udffb-\\udfff])?(?:\\u200d(?:[^\\ud800-\\udfff]|(?:\\ud83c[\\udde6-\\uddff]){2}|[\\ud800-\\udbff][\\udc00-\\udfff])[\\ufe0e\\ufe0f]?(?:[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff\\u1ab0-\\u1aff\\u1dc0-\\u1dff]|\\ud83c[\\udffb-\\udfff])?)*");
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<path android:pathData="M12 1.998c5.524 0 10.002 4.478 10.002 10.002 0 5.523-4.478 10-10.002 10-5.524 0.001-10.002-4.477-10.002-10C1.998 6.476 6.476 1.998 12 1.998zM8.462 14.783c-0.257-0.325-0.728-0.381-1.053-0.125-0.326 0.256-0.382 0.728-0.125 1.053 1.13 1.435 2.853 2.29 4.716 2.29 1.86 0 3.581-0.853 4.712-2.284 0.257-0.325 0.201-0.797-0.124-1.054-0.325-0.256-0.796-0.201-1.053 0.124-0.85 1.075-2.139 1.714-3.535 1.714-1.398 0-2.69-0.64-3.538-1.718zM9 8.75c-0.69 0-1.25 0.56-1.25 1.25S8.31 11.249 9 11.249s1.249-0.56 1.249-1.25S9.69 8.75 9 8.75zm6 0c-0.69 0-1.25 0.56-1.25 1.25s0.56 1.249 1.25 1.249 1.249-0.56 1.249-1.25S15.69 8.75 15 8.75z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
|
@ -0,0 +1,3 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<path android:pathData="M12 1.998c5.524 0 10.002 4.478 10.002 10.002 0 5.523-4.478 10-10.002 10-5.524 0.001-10.002-4.477-10.002-10C1.998 6.476 6.476 1.998 12 1.998zm0 1.5c-4.695 0-8.502 3.806-8.502 8.502 0 4.695 3.807 8.501 8.502 8.501s8.502-3.806 8.502-8.501c0-4.696-3.807-8.502-8.502-8.502zM8.462 14.783C9.31 15.86 10.602 16.5 12 16.5c1.396 0 2.686-0.639 3.535-1.714 0.257-0.325 0.728-0.38 1.053-0.124 0.325 0.257 0.38 0.729 0.124 1.054C15.582 17.148 13.86 18 12 18c-1.863 0-3.586-0.855-4.716-2.29-0.257-0.325-0.2-0.797 0.125-1.053 0.325-0.256 0.796-0.2 1.053 0.125zM9 8.75c0.69 0 1.249 0.56 1.249 1.25S9.69 11.248 9 11.248s-1.249-0.56-1.249-1.25 0.56-1.249 1.25-1.249zm6 0c0.69 0 1.249 0.56 1.249 1.25s-0.56 1.249-1.25 1.249-1.249-0.56-1.249-1.25S14.31 8.75 15 8.75z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--~ Copyright (c) 2022. ~ Microsoft Corporation. All rights reserved.-->
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/ic_fluent_emoji_24_filled" android:state_activated="true"/>
|
||||
<item android:drawable="@drawable/ic_fluent_emoji_24_filled" android:state_checked="true"/>
|
||||
<item android:drawable="@drawable/ic_fluent_emoji_24_filled" android:state_selected="true"/>
|
||||
<item android:drawable="@drawable/ic_fluent_emoji_24_regular"/>
|
||||
</selector>
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<org.joinmastodon.android.ui.views.SizeListenerLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -57,18 +57,41 @@
|
|||
android:background="@null"
|
||||
android:inputType="textMultiLine|textCapSentences"/>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="#20000000"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="48dp"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp">
|
||||
android:gravity="center_vertical"
|
||||
android:background="@color/gray_25"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/btn_emoji"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||
android:padding="0px"
|
||||
android:src="@drawable/ic_fluent_emoji_24_selector"/>
|
||||
|
||||
<View
|
||||
android:layout_width="0px"
|
||||
android:layout_height="1px"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/char_counter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/m3_body_large"
|
||||
android:textColor="@color/gray_500"
|
||||
tools:text="500"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</org.joinmastodon.android.ui.views.SizeListenerLinearLayout>
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:textSize="12dp"
|
||||
android:textColor="@color/gray_500"
|
||||
android:textAllCaps="true"
|
||||
android:paddingTop="24dp"
|
||||
android:paddingBottom="12dp"
|
||||
tools:text="Blob whatever things"/>
|
|
@ -10,9 +10,11 @@
|
|||
|
||||
<color name="fluent_default_icon_tint">@color/gray_800</color>
|
||||
|
||||
<color name="gray_25">#FCFCFD</color>
|
||||
<color name="gray_50t">#CCF9FAFB</color>
|
||||
<color name="gray_100">#F2F4F7</color>
|
||||
<color name="gray_800">#282C37</color>
|
||||
<color name="gray_500">#667085</color>
|
||||
<color name="gray_50t">#CCF9FAFB</color>
|
||||
|
||||
<color name="text_primary">@color/gray_800</color>
|
||||
<color name="text_secondary">@color/gray_500</color>
|
||||
|
|
Loading…
Reference in New Issue