mirror of
https://github.com/tateisu/SubwayTooter
synced 2025-02-05 21:23:26 +01:00
日本語のハッシュタグの入力時にキーボードを切り替えるのが面倒なので投稿画面の#ボタンを常に表示して、押した先のメニュー項目で#だけを入力できるようにする。ついでにMisskeyでもハッシュタグトレンドをこのメニューに表示する。
This commit is contained in:
parent
57fc721ec2
commit
e47f96aa64
@ -18,13 +18,13 @@ class TestEntityUtils {
|
||||
|
||||
class TestEntity(val s : String, val l : Long) : Mappable<String> {
|
||||
constructor(src : JsonObject) : this(
|
||||
s = src.notEmptyOrThrow("s"),
|
||||
s = src.stringOrThrow("s"),
|
||||
l = src.long("l") ?: 0L
|
||||
)
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
constructor(parser : TootParser, src : JsonObject) : this(
|
||||
s = src.notEmptyOrThrow("s"),
|
||||
s = src.stringOrThrow("s"),
|
||||
l = src.long("l") ?: 0L
|
||||
)
|
||||
|
||||
@ -254,16 +254,16 @@ class TestEntityUtils {
|
||||
|
||||
@Test(expected = RuntimeException::class)
|
||||
fun testNotEmptyOrThrow4() {
|
||||
println("""{"param1":null}""".decodeJsonObject().notEmptyOrThrow("param1"))
|
||||
println("""{"param1":null}""".decodeJsonObject().stringOrThrow("param1"))
|
||||
}
|
||||
|
||||
@Test(expected = RuntimeException::class)
|
||||
fun testNotEmptyOrThrow5() {
|
||||
println("""{"param1":""}""".decodeJsonObject().notEmptyOrThrow("param1"))
|
||||
println("""{"param1":""}""".decodeJsonObject().stringOrThrow("param1"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testNotEmptyOrThrow6() {
|
||||
assertEquals("A", """{"param1":"A"}""".decodeJsonObject().notEmptyOrThrow("param1"))
|
||||
assertEquals("A", """{"param1":"A"}""".decodeJsonObject().stringOrThrow("param1"))
|
||||
}
|
||||
}
|
@ -1428,14 +1428,11 @@ class ActPost : AsyncActivity(),
|
||||
|
||||
val account = account
|
||||
if(account == null || account.isPseudo) {
|
||||
btnFeaturedTag.vg(false)
|
||||
return
|
||||
}
|
||||
|
||||
val cache = featuredTagCache[account.acct.ascii]
|
||||
|
||||
btnFeaturedTag.vg(cache?.list?.isNotEmpty() == true)
|
||||
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
if(cache != null && now - cache.time <= 300000L) return
|
||||
|
||||
@ -1448,12 +1445,18 @@ class ActPost : AsyncActivity(),
|
||||
|
||||
override fun background(client : TootApiClient) : TootApiResult? {
|
||||
return if(account.isMisskey) {
|
||||
featuredTagCache[account.acct.ascii] =
|
||||
FeaturedTagCache(emptyList(), SystemClock.elapsedRealtime())
|
||||
TootApiResult()
|
||||
client.request(
|
||||
"/api/hashtags/trend",
|
||||
jsonObject { }
|
||||
.toPostRequestBuilder()
|
||||
)?.also { result ->
|
||||
val list = TootTag.parseList( TootParser(this@ActPost,account), result.jsonArray)
|
||||
featuredTagCache[account.acct.ascii] =
|
||||
FeaturedTagCache(list, SystemClock.elapsedRealtime())
|
||||
}
|
||||
} else {
|
||||
client.request("/api/v1/featured_tags")?.also { result ->
|
||||
val list = parseList(::TootTag, result.jsonArray)
|
||||
val list = TootTag.parseList( TootParser(this@ActPost,account), result.jsonArray)
|
||||
featuredTagCache[account.acct.ascii] =
|
||||
FeaturedTagCache(list, SystemClock.elapsedRealtime())
|
||||
}
|
||||
|
@ -1502,9 +1502,10 @@ class Column(
|
||||
val re = Pattern.compile(regex_text)
|
||||
column_regex_filter =
|
||||
{ text : CharSequence? ->
|
||||
if(text?.isEmpty() != false) false else re.matcher(
|
||||
text
|
||||
).find()
|
||||
if(text?.isEmpty() != false)
|
||||
false
|
||||
else
|
||||
re.matcher(text).find()
|
||||
}
|
||||
} catch(ex : Throwable) {
|
||||
log.trace(ex)
|
||||
@ -1967,8 +1968,6 @@ class Column(
|
||||
task.start()
|
||||
}
|
||||
|
||||
private var bMinIdMatched : Boolean = false
|
||||
|
||||
internal fun parseRange(
|
||||
result : TootApiResult?,
|
||||
list : List<TimelineItem>?
|
||||
@ -1978,30 +1977,29 @@ class Column(
|
||||
|
||||
if(isMisskey && list != null) {
|
||||
// MisskeyはLinkヘッダがないので、常にデータからIDを読む
|
||||
|
||||
for(item in list) {
|
||||
|
||||
// injectされたデータをデータ範囲に追加しない
|
||||
if( item .isInjected() ) continue
|
||||
|
||||
if(item.isInjected()) continue
|
||||
|
||||
val id = item.getOrderId()
|
||||
if( id.notDefaultOrConfirming){
|
||||
if(id.notDefaultOrConfirming) {
|
||||
if(idMin == null || id < idMin) idMin = id
|
||||
if(idMax == null || id > idMax) idMax = id
|
||||
}
|
||||
}
|
||||
} else if(result != null) {
|
||||
} else {
|
||||
// Linkヘッダを読む
|
||||
idMin = reMaxId.matcher(result?.link_older?:"").findOrNull()
|
||||
?.let{
|
||||
idMin = reMaxId.matcher(result?.link_older ?: "").findOrNull()
|
||||
?.let {
|
||||
EntityId(it.groupEx(1) !!)
|
||||
}
|
||||
|
||||
idMax =reMinId.matcher(result.link_newer ?: "").findOrNull()
|
||||
|
||||
idMax = reMinId.matcher(result?.link_newer ?: "").findOrNull()
|
||||
?.let {
|
||||
bMinIdMatched = it.groupEx(1)=="min_id"
|
||||
// min_idとsince_idの読み分けは現在利用してない it.groupEx(1)=="min_id"
|
||||
EntityId(it.groupEx(2) !!)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return Pair(idMin, idMax)
|
||||
|
@ -46,7 +46,7 @@ class TootParser(
|
||||
fun notification(src : JsonObject?) = parseItem(::TootNotification, this, src)
|
||||
fun notificationList(src : JsonArray?) = parseList(::TootNotification, this, src)
|
||||
|
||||
fun tagList(array : JsonArray?) = parseList(::TootTag, array)
|
||||
fun tagList(array : JsonArray?) = TootTag.parseList(this, array)
|
||||
fun results(src : JsonObject?) = parseItem(::TootResults, this, src)
|
||||
fun instance(src : JsonObject?) = parseItem(::TootInstance, this, src)
|
||||
|
||||
|
@ -28,8 +28,8 @@ class CustomEmoji(
|
||||
|
||||
val decode : (JsonObject) -> CustomEmoji = { src ->
|
||||
CustomEmoji(
|
||||
shortcode = src.notEmptyOrThrow("shortcode"),
|
||||
url = src.notEmptyOrThrow("url"),
|
||||
shortcode = src.stringOrThrow("shortcode"),
|
||||
url = src.stringOrThrow("url"),
|
||||
static_url = src.string("static_url"),
|
||||
visible_in_picker = src.optBoolean("visible_in_picker", true),
|
||||
category =src.string("category")
|
||||
|
@ -44,7 +44,7 @@ class MisskeyAntenna(src : JsonObject) :TimelineItem(){
|
||||
|
||||
init{
|
||||
timeCreatedAt = TootStatus.parseTime(src.string("createdAt"))
|
||||
id = EntityId(src.notEmptyOrThrow("id"))
|
||||
id = EntityId(src.stringOrThrow("id"))
|
||||
name = src.string("name") ?: "?"
|
||||
this.src = src.string("src") ?: "?"
|
||||
|
||||
|
@ -10,8 +10,8 @@ class NicoProfileEmoji(
|
||||
) : Mappable<String> {
|
||||
|
||||
constructor(src : JsonObject, shortcode : String? = null) : this(
|
||||
url = src.notEmptyOrThrow("url"),
|
||||
shortcode = shortcode ?: src.notEmptyOrThrow("shortcode"),
|
||||
url = src.stringOrThrow("url"),
|
||||
shortcode = shortcode ?: src.stringOrThrow("shortcode"),
|
||||
account_url = src.string("account_url"),
|
||||
account_id = EntityId.mayDefault(src.string("account_id"))
|
||||
)
|
||||
|
@ -134,7 +134,7 @@ open class TootAccount(parser : TootParser, src : JsonObject) {
|
||||
parseMapOrNull(CustomEmoji.decodeMisskey, src.jsonArray("emojis"))
|
||||
this.profile_emojis = null
|
||||
|
||||
this.username = src.notEmptyOrThrow("username")
|
||||
this.username = src.stringOrThrow("username")
|
||||
|
||||
this.apiHost = src.string("host")?.let { Host.parse(it) }
|
||||
?: parser.apiHost
|
||||
@ -229,7 +229,7 @@ open class TootAccount(parser : TootParser, src : JsonObject) {
|
||||
|
||||
// 疑似アカウントにacctとusernameだけ
|
||||
this.url = src.string("url")
|
||||
this.username = src.notEmptyOrThrow("username")
|
||||
this.username = src.stringOrThrow("username")
|
||||
|
||||
//
|
||||
val sv = src.string("display_name")
|
||||
@ -262,7 +262,7 @@ open class TootAccount(parser : TootParser, src : JsonObject) {
|
||||
|
||||
this.id = EntityId.mayDefault(src.string("id"))
|
||||
|
||||
val tmpAcct = src.notEmptyOrThrow("acct")
|
||||
val tmpAcct = src.stringOrThrow("acct")
|
||||
|
||||
val(apiHost,apDomain) = findHostFromUrl(tmpAcct, parser.linkHelper, url)
|
||||
apiHost ?: error("can't get apiHost from acct or url")
|
||||
@ -290,7 +290,7 @@ open class TootAccount(parser : TootParser, src : JsonObject) {
|
||||
// tootsearch のアカウントのIDはどのタンス上のものか分からないので役に立たない
|
||||
this.id = EntityId.DEFAULT
|
||||
|
||||
val tmpAcct = src.notEmptyOrThrow("acct")
|
||||
val tmpAcct = src.stringOrThrow("acct")
|
||||
|
||||
val(apiHost,apDomain) = findHostFromUrl(tmpAcct, null, url)
|
||||
apiHost ?: error("can't get apiHost from acct or url")
|
||||
|
@ -50,12 +50,11 @@ class TootAnnouncement(parser : TootParser, src : JsonObject) {
|
||||
val decoded_content : Spannable
|
||||
|
||||
//An array of Tags
|
||||
val tags : ArrayList<TootTag>?
|
||||
val tags : List<TootTag>?
|
||||
|
||||
// An array of Mentions
|
||||
val mentions : ArrayList<TootMention>?
|
||||
|
||||
|
||||
var reactions : MutableList<Reaction>? = null
|
||||
|
||||
init {
|
||||
@ -63,7 +62,7 @@ class TootAnnouncement(parser : TootParser, src : JsonObject) {
|
||||
this.custom_emojis =
|
||||
parseMapOrNull(CustomEmoji.decode, src.jsonArray("emojis"), log)
|
||||
|
||||
this.tags = parseListOrNull(::TootTag, src.jsonArray("tags"))
|
||||
this.tags = TootTag.parseListOrNull(parser,src.jsonArray("tags"))
|
||||
|
||||
this.mentions = parseListOrNull(::TootMention, src.jsonArray("mentions"), log)
|
||||
|
||||
|
@ -18,8 +18,8 @@ class TootMention(
|
||||
|
||||
constructor(src : JsonObject) : this(
|
||||
id = EntityId.mayDefault(src.string("id")),
|
||||
url = src.notEmptyOrThrow("url"),
|
||||
acct = Acct.parse(src.notEmptyOrThrow("acct")),
|
||||
username = src.notEmptyOrThrow("username")
|
||||
url = src.stringOrThrow("url"),
|
||||
acct = Acct.parse(src.stringOrThrow("acct")),
|
||||
username = src.stringOrThrow("username")
|
||||
)
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ class TootNotification(parser : TootParser, src : JsonObject) : TimelineItem() {
|
||||
if(parser.serviceType == ServiceType.MISSKEY) {
|
||||
id = EntityId.mayDefault(src.string("id"))
|
||||
|
||||
type = src.notEmptyOrThrow("type")
|
||||
type = src.stringOrThrow("type")
|
||||
|
||||
created_at = src.string("createdAt")
|
||||
time_created_at = TootStatus.parseTime(created_at)
|
||||
@ -83,7 +83,7 @@ class TootNotification(parser : TootParser, src : JsonObject) : TimelineItem() {
|
||||
} else {
|
||||
id = EntityId.mayDefault(src.string("id"))
|
||||
|
||||
type = src.notEmptyOrThrow("type")
|
||||
type = src.stringOrThrow("type")
|
||||
|
||||
created_at = src.string("created_at")
|
||||
time_created_at = TootStatus.parseTime(created_at)
|
||||
|
@ -10,7 +10,7 @@ class TootResults private constructor(
|
||||
// An array of matched Statuses
|
||||
val statuses : ArrayList<TootStatus>,
|
||||
// An array of matched hashtags
|
||||
val hashtags : ArrayList<TootTag>
|
||||
val hashtags : List<TootTag>
|
||||
) {
|
||||
|
||||
var searchApiVersion = 0 // 0 means not from search API. such as trend tags.
|
||||
|
@ -138,7 +138,7 @@ class TootStatus(parser : TootParser, src : JsonObject) : TimelineItem() {
|
||||
var mentions : ArrayList<TootMention>? = null
|
||||
|
||||
//An array of Tags
|
||||
var tags : ArrayList<TootTag>? = null
|
||||
var tags : List<TootTag>? = null
|
||||
|
||||
// public Spannable decoded_tags;
|
||||
var decoded_mentions : Spannable = EMPTY_SPANNABLE
|
||||
@ -513,7 +513,7 @@ class TootStatus(parser : TootParser, src : JsonObject) : TimelineItem() {
|
||||
this.in_reply_to_account_id =
|
||||
EntityId.mayNull(src.string("in_reply_to_account_id"))
|
||||
this.mentions = parseListOrNull(::TootMention, src.jsonArray("mentions"), log)
|
||||
this.tags = parseListOrNull(::TootTag, src.jsonArray("tags"))
|
||||
this.tags = TootTag.parseListOrNull(parser,src.jsonArray("tags"))
|
||||
this.application =
|
||||
parseItem(::TootApplication, parser, src.jsonObject("application"), log)
|
||||
this.pinned = parser.pinned || src.optBoolean("pinned")
|
||||
|
@ -1,5 +1,6 @@
|
||||
package jp.juggler.subwaytooter.api.entity
|
||||
|
||||
import android.net.Uri
|
||||
import jp.juggler.subwaytooter.api.TootParser
|
||||
import jp.juggler.subwaytooter.util.MisskeyMarkdownDecoder
|
||||
import jp.juggler.util.*
|
||||
@ -32,6 +33,7 @@ open class TootTag constructor(
|
||||
}
|
||||
|
||||
class History(src : JsonObject) {
|
||||
|
||||
private val day : Long
|
||||
val uses : Int
|
||||
val accounts : Int
|
||||
@ -47,17 +49,27 @@ open class TootTag constructor(
|
||||
|
||||
}
|
||||
|
||||
// for TREND_TAG column
|
||||
constructor(src : JsonObject) : this(
|
||||
name = src.notEmptyOrThrow("name"),
|
||||
url = src.string("url"),
|
||||
history = parseHistories(src.jsonArray("history"))
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
||||
val log = LogCategory("TootTag")
|
||||
|
||||
fun parse(parser : TootParser, src : JsonObject) =
|
||||
if(parser.linkHelper.isMisskey) {
|
||||
val name = src.stringOrThrow("tag")
|
||||
val url = "https://${parser.apiHost}/tags/${Uri.encode(name)}"
|
||||
TootTag(
|
||||
name = name,
|
||||
url = url,
|
||||
history = null
|
||||
)
|
||||
} else {
|
||||
TootTag(
|
||||
name = src.stringOrThrow("name"),
|
||||
url = src.string("url"),
|
||||
history = parseHistories(src.jsonArray("history"))
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseHistories(src : JsonArray?) : ArrayList<History>? {
|
||||
src ?: return null
|
||||
|
||||
@ -72,45 +84,32 @@ open class TootTag constructor(
|
||||
return dst
|
||||
}
|
||||
|
||||
fun parseList(parser : TootParser, array : JsonArray?) : ArrayList<TootTag> {
|
||||
val result = ArrayList<TootTag>()
|
||||
if(array != null) {
|
||||
if(parser.serviceType == ServiceType.MISSKEY) {
|
||||
array.stringArrayList().forEach { sv ->
|
||||
if(sv.isNotEmpty()) {
|
||||
result.add(TootTag(name = sv))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
array.forEach { item ->
|
||||
val tag = try {
|
||||
when(item) {
|
||||
is String -> if(item.isNotEmpty()) {
|
||||
TootTag(name = item)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
is JsonObject -> TootTag(item)
|
||||
else -> null
|
||||
}
|
||||
} catch(ex : Throwable) {
|
||||
log.w(ex, "parseList: parse error")
|
||||
null
|
||||
}
|
||||
if(tag != null) result.add(tag)
|
||||
fun parseListOrNull(parser : TootParser, array : JsonArray?) =
|
||||
array?.mapNotNull { src->
|
||||
try {
|
||||
when(src) {
|
||||
null -> null
|
||||
"" -> null
|
||||
is String -> TootTag(name = src)
|
||||
is JsonObject -> parse(parser, src)
|
||||
else->null
|
||||
}
|
||||
}catch(ex:Throwable){
|
||||
log.e(ex,"parseListOrNull failed.")
|
||||
null
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}?.notEmpty()
|
||||
|
||||
fun parseList(parser : TootParser, array : JsonArray?) =
|
||||
parseListOrNull(parser,array) ?: emptyList()
|
||||
|
||||
private const val w = TootAccount.reRubyWord
|
||||
private const val a = TootAccount.reRubyAlpha
|
||||
private const val s = "_\\u00B7\\u200c" // separators
|
||||
|
||||
private fun generateMastodonTagPattern():Pattern{
|
||||
|
||||
private fun generateMastodonTagPattern() : Pattern {
|
||||
val reMastodonTagName = """([_$w][$s$w]*[$s$a][$s$w]*[_$w])|([_$w]*[$a][_$w]*)"""
|
||||
return """(?:^|[^\w/)])#($reMastodonTagName)""".asciiPattern()
|
||||
return """(?:^|[^\w/)])#($reMastodonTagName)""".asciiPattern()
|
||||
}
|
||||
|
||||
private val reMastodonTag = generateMastodonTagPattern()
|
||||
@ -157,12 +156,13 @@ open class TootTag constructor(
|
||||
|
||||
// https://mastodon.juggler.jp/tags/%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E3%82%BF%E3%82%B0
|
||||
// あるサービスは /tags/... でなく /tag/... を使う
|
||||
private val reUrlHashTag ="""\Ahttps://([^/]+)/tags?/([^?#・\s\-+.,:;/]+)(?:\z|[?#])"""
|
||||
private val reUrlHashTag = """\Ahttps://([^/]+)/tags?/([^?#・\s\-+.,:;/]+)(?:\z|[?#])"""
|
||||
.asciiPattern()
|
||||
|
||||
// https://pixelfed.tokyo/discover/tags/SubwayTooter?src=hash
|
||||
private val reUrlHashTagPixelfed ="""\Ahttps://([^/]+)/discover/tags/([^?#・\s\-+.,:;/]+)(?:\z|[?#])"""
|
||||
.asciiPattern()
|
||||
private val reUrlHashTagPixelfed =
|
||||
"""\Ahttps://([^/]+)/discover/tags/([^?#・\s\-+.,:;/]+)(?:\z|[?#])"""
|
||||
.asciiPattern()
|
||||
|
||||
// returns null or pair of ( decoded tag without sharp, host)
|
||||
fun String.findHashtagFromUrl() : Pair<String, String>? {
|
||||
|
@ -160,7 +160,7 @@ class HighlightWord {
|
||||
|
||||
constructor(src : JsonObject) {
|
||||
this.id = src.long(COL_ID) ?: - 1L
|
||||
this.name = src.notEmptyOrThrow(COL_NAME)
|
||||
this.name = src.stringOrThrow(COL_NAME)
|
||||
this.color_bg = src.optInt(COL_COLOR_BG)
|
||||
this.color_fg = src.optInt(COL_COLOR_FG)
|
||||
this.sound_type = src.optInt(COL_SOUND_TYPE)
|
||||
|
@ -1090,9 +1090,8 @@ class PostHelper(
|
||||
}
|
||||
|
||||
fun openFeaturedTagList(list : List<TootTag>?) {
|
||||
if(list?.isEmpty() != false) return
|
||||
val ad = ActionsDialog()
|
||||
for(tag in list) {
|
||||
list?.forEach {tag->
|
||||
ad.addAction("#${tag.name}") {
|
||||
val et = this.et ?: return@addAction
|
||||
|
||||
@ -1113,6 +1112,27 @@ class PostHelper(
|
||||
proc_text_changed.run()
|
||||
}
|
||||
}
|
||||
ad.addAction( activity.getString(R.string.input_sharp_itself)){
|
||||
val et = this.et ?: return@addAction
|
||||
|
||||
val src = et.text ?: ""
|
||||
val src_length = src.length
|
||||
val start = min(src_length, et.selectionStart)
|
||||
val end = min(src_length, et.selectionEnd)
|
||||
|
||||
val sb = SpannableStringBuilder()
|
||||
sb.append(src.subSequence(0, start))
|
||||
if(! EmojiDecoder.canStartHashtag(sb, sb.length)) sb.append(' ')
|
||||
sb.append('#')
|
||||
|
||||
val newSelection = sb.length
|
||||
if(end < src_length) sb.append(src.subSequence(end, src_length))
|
||||
et.text = sb
|
||||
et.setSelection(newSelection)
|
||||
|
||||
proc_text_changed.run()
|
||||
|
||||
}
|
||||
ad.show(activity, activity.getString(R.string.featured_hashtags))
|
||||
}
|
||||
|
||||
|
@ -270,7 +270,7 @@ class JsonObject : LinkedHashMap<String, Any?>() {
|
||||
@Suppress("unused")
|
||||
fun optDouble(name : String, defVal : Double = 0.0) = double(name) ?: defVal
|
||||
|
||||
fun notEmptyOrThrow(name : String) = notEmptyOrThrow(name, string(name))
|
||||
fun stringOrThrow(name : String) = notEmptyOrThrow(name, string(name))
|
||||
fun isNull(name : String) = this[name] == null
|
||||
fun putNotNull(name : String, value : Any?) {
|
||||
if(value != null) put(name, value)
|
||||
|
@ -1044,4 +1044,5 @@
|
||||
<string name="gap_head">ギャップの始端</string>
|
||||
<string name="gap_tail">ギャップの終端</string>
|
||||
<string name="ignore_text_in_shared_media">外部アプリから共有されたメディアに付属する余計なテキストを追加しない</string>
|
||||
<string name="input_sharp_itself">\'#\'だけを入力</string>
|
||||
</resources>
|
||||
|
@ -1052,4 +1052,5 @@
|
||||
<string name="gap_head">Head of gap</string>
|
||||
<string name="gap_tail">Tail of gap</string>
|
||||
<string name="ignore_text_in_shared_media">Don\'t append extra text that come with shared media from external apps</string>
|
||||
<string name="input_sharp_itself">Input \'#\' itself</string>
|
||||
</resources>
|
||||
|
Loading…
x
Reference in New Issue
Block a user