カラムがストリーミングイベントを受け取った際にカラムの状態を確認する
This commit is contained in:
parent
c3ea720265
commit
00b05d8646
|
@ -372,11 +372,8 @@ class Column(
|
|||
// カラムオブジェクトの識別に使うID。
|
||||
val internalId = internalIdSeed.incrementAndGet()
|
||||
|
||||
|
||||
val type = ColumnType.parse(typeId)
|
||||
|
||||
|
||||
|
||||
internal var dont_close: Boolean = false
|
||||
|
||||
internal var with_attachment: Boolean = false
|
||||
|
@ -553,16 +550,409 @@ class Column(
|
|||
private val last_show_stream_data = AtomicLong(0L)
|
||||
private val stream_data_queue = ConcurrentLinkedQueue<TimelineItem>()
|
||||
|
||||
@Volatile
|
||||
private var bPutGap: Boolean = false
|
||||
|
||||
val isSearchColumn: Boolean
|
||||
get() {
|
||||
return when (type) {
|
||||
ColumnType.SEARCH, ColumnType.SEARCH_MSP, ColumnType.SEARCH_TS, ColumnType.SEARCH_NOTESTOCK -> true
|
||||
else -> false
|
||||
private var cacheHeaderDesc: String? = null
|
||||
|
||||
// DMカラム更新時に新APIの利用に成功したなら真
|
||||
internal var useConversationSummarys = false
|
||||
|
||||
// DMカラムのストリーミングイベントで新形式のイベントを利用できたなら真
|
||||
internal var useConversationSummaryStreaming = false
|
||||
|
||||
////////////////////////////////////////////////////////////////
|
||||
|
||||
private fun canHandleStreamingMessage () = !is_dispose.get() && canStartStreaming()
|
||||
|
||||
private fun runOnMainLooperForStreamingEvent(proc : () -> Unit){
|
||||
runOnMainLooper {
|
||||
if(!canHandleStreamingMessage() )
|
||||
return@runOnMainLooper
|
||||
proc()
|
||||
}
|
||||
}
|
||||
|
||||
val streamCallback = object : StreamCallback {
|
||||
|
||||
override fun onListeningStateChanged(status: StreamStatus) {
|
||||
if(!canHandleStreamingMessage() ) return
|
||||
|
||||
if (status == StreamStatus.Open) {
|
||||
updateMisskeyCapture()
|
||||
}
|
||||
|
||||
runOnMainLooperForStreamingEvent {
|
||||
fireShowColumnStatus()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTimelineItem(item: TimelineItem, channelId: String?,stream:JsonArray?) {
|
||||
if(!canHandleStreamingMessage() ) return
|
||||
|
||||
when (item) {
|
||||
is TootConversationSummary -> {
|
||||
if (type != ColumnType.DIRECT_MESSAGES) return
|
||||
if (isFiltered(item.last_status)) return
|
||||
if (use_old_api) {
|
||||
useConversationSummaryStreaming = false
|
||||
return
|
||||
} else {
|
||||
useConversationSummaryStreaming = true
|
||||
}
|
||||
}
|
||||
|
||||
is TootNotification -> {
|
||||
if (!isNotificationColumn) return
|
||||
if (isFiltered(item)) return
|
||||
}
|
||||
|
||||
is TootStatus -> {
|
||||
if (isNotificationColumn) return
|
||||
|
||||
// マストドン2.6.0形式のDMカラム用イベントを利用したならば、その直後に発生する普通の投稿イベントを無視する
|
||||
if (useConversationSummaryStreaming) return
|
||||
|
||||
// マストドンはLTLに外部ユーザの投稿を表示しない
|
||||
if (type == ColumnType.LOCAL && isMastodon && item.account.isRemote) return
|
||||
|
||||
if (isFiltered(item)) return
|
||||
}
|
||||
}
|
||||
|
||||
stream_data_queue.add(item)
|
||||
app_state.handler.post(mergeStreamingMessage)
|
||||
}
|
||||
|
||||
override fun onNoteUpdated(ev: MisskeyNoteUpdate, channelId: String?) {
|
||||
runOnMainLooperForStreamingEvent {
|
||||
|
||||
// userId が自分かどうか調べる
|
||||
// アクセストークンの更新をして自分のuserIdが分かる状態でないとキャプチャ結果を反映させない
|
||||
// (でないとリアクションの2重カウントなどが発生してしまう)
|
||||
val myId = EntityId.from(access_info.token_info, TootApiClient.KEY_USER_ID)
|
||||
if (myId == null) {
|
||||
log.w("onNoteUpdated: missing my userId. updating access token is recommenced!!")
|
||||
}
|
||||
|
||||
val byMe = myId == ev.userId
|
||||
|
||||
val changeList = ArrayList<AdapterChange>()
|
||||
|
||||
fun scanStatus1(s: TootStatus?, idx: Int, block: (s: TootStatus) -> Boolean) {
|
||||
s ?: return
|
||||
if (s.id == ev.noteId) {
|
||||
if (block(s)) {
|
||||
changeList.add(AdapterChange(AdapterChangeType.RangeChange, idx, 1))
|
||||
}
|
||||
}
|
||||
scanStatus1(s.reblog, idx, block)
|
||||
scanStatus1(s.reply, idx, block)
|
||||
}
|
||||
|
||||
fun scanStatusAll(block: (s: TootStatus) -> Boolean) {
|
||||
for (i in 0 until list_data.size) {
|
||||
val o = list_data[i]
|
||||
if (o is TootStatus) {
|
||||
scanStatus1(o, i, block)
|
||||
} else if (o is TootNotification) {
|
||||
scanStatus1(o.status, i, block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (ev.type) {
|
||||
MisskeyNoteUpdate.Type.REACTION -> {
|
||||
scanStatusAll { s ->
|
||||
s.increaseReaction(ev.reaction, byMe, "onNoteUpdated ${ev.userId}")
|
||||
}
|
||||
}
|
||||
|
||||
MisskeyNoteUpdate.Type.UNREACTION -> {
|
||||
scanStatusAll { s ->
|
||||
s.decreaseReaction(ev.reaction, byMe, "onNoteUpdated ${ev.userId}")
|
||||
}
|
||||
}
|
||||
|
||||
MisskeyNoteUpdate.Type.VOTED -> {
|
||||
scanStatusAll { s ->
|
||||
s.enquete?.increaseVote(context, ev.choice, byMe) ?: false
|
||||
}
|
||||
}
|
||||
|
||||
MisskeyNoteUpdate.Type.DELETED -> {
|
||||
scanStatusAll { s ->
|
||||
s.markDeleted(context, ev.deletedAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changeList.isNotEmpty()) {
|
||||
fireShowContent(reason = "onNoteUpdated", changeList = changeList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAnnouncementUpdate(item: TootAnnouncement) {
|
||||
runOnMainLooperForStreamingEvent {
|
||||
if( type != ColumnType.HOME)
|
||||
return@runOnMainLooperForStreamingEvent
|
||||
|
||||
val list = announcements
|
||||
if (list == null) {
|
||||
announcements = mutableListOf(item)
|
||||
} else {
|
||||
val index = list.indexOfFirst { it.id == item.id }
|
||||
list.add(
|
||||
0,
|
||||
if (index == -1) {
|
||||
item
|
||||
} else {
|
||||
TootAnnouncement.merge(list.removeAt(index), item)
|
||||
}
|
||||
)
|
||||
}
|
||||
announcementUpdated = SystemClock.elapsedRealtime()
|
||||
fireShowColumnHeader()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAnnouncementDelete(id: EntityId) {
|
||||
runOnMainLooperForStreamingEvent {
|
||||
announcements?.iterator()?.let{
|
||||
while (it.hasNext()) {
|
||||
val item = it.next()
|
||||
if (item.id != id) continue
|
||||
it.remove()
|
||||
announcementUpdated = SystemClock.elapsedRealtime()
|
||||
fireShowColumnHeader()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAnnouncementReaction(reaction: TootAnnouncement.Reaction) {
|
||||
runOnMainLooperForStreamingEvent {
|
||||
// find announcement
|
||||
val announcement_id = reaction.announcement_id
|
||||
?: return@runOnMainLooperForStreamingEvent
|
||||
val announcement = announcements?.find { it.id == announcement_id }
|
||||
?: return@runOnMainLooperForStreamingEvent
|
||||
|
||||
// find reaction
|
||||
val index = announcement.reactions?.indexOfFirst { it.name == reaction.name }
|
||||
when {
|
||||
reaction.count <= 0L -> {
|
||||
if (index != null && index != -1) announcement.reactions?.removeAt(index)
|
||||
}
|
||||
|
||||
index == null -> {
|
||||
announcement.reactions = ArrayList<TootAnnouncement.Reaction>().apply {
|
||||
add(reaction)
|
||||
}
|
||||
}
|
||||
|
||||
index == -1 -> announcement.reactions?.add(reaction)
|
||||
|
||||
else -> announcement.reactions?.get(index)?.let { old ->
|
||||
old.count = reaction.count
|
||||
// ストリーミングイベントにはmeが含まれないので、oldにあるmeは変更されない
|
||||
}
|
||||
}
|
||||
announcementUpdated = SystemClock.elapsedRealtime()
|
||||
fireShowColumnHeader()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val mergeStreamingMessage: Runnable = object : Runnable {
|
||||
override fun run() {
|
||||
|
||||
// 前回マージしてから暫くは待機してリトライ
|
||||
val handler = app_state.handler
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
val remain = last_show_stream_data.get() + 333L - now
|
||||
if (remain > 0) {
|
||||
handler.removeCallbacks(this)
|
||||
handler.postDelayed(this, remain)
|
||||
return
|
||||
}
|
||||
|
||||
// カラムがビジー状態なら待機してリトライ
|
||||
if( !canStartStreaming() || bRefreshLoading){
|
||||
handler.removeCallbacks(this)
|
||||
handler.postDelayed(this, 333L)
|
||||
return
|
||||
}
|
||||
|
||||
last_show_stream_data.set(now)
|
||||
|
||||
val tmpList = ArrayList<TimelineItem>()
|
||||
while (true) tmpList.add(stream_data_queue.poll() ?: break)
|
||||
if (tmpList.isEmpty()) return
|
||||
|
||||
// キューから読めた件数が0の場合を除き、少し後に再処理させることでマージ漏れを防ぐ
|
||||
handler.postDelayed(this, 333L)
|
||||
|
||||
// ストリーミングされるデータは全てID順に並んでいるはず
|
||||
tmpList.sortByDescending { it.getOrderId() }
|
||||
|
||||
val list_new = duplicate_map.filterDuplicate(tmpList)
|
||||
if (list_new.isEmpty()) return
|
||||
|
||||
for (item in list_new) {
|
||||
if (enable_speech && item is TootStatus) {
|
||||
app_state.addSpeech(item.reblog ?: item)
|
||||
}
|
||||
}
|
||||
|
||||
// 通知カラムならストリーミング経由で届いたデータを通知ワーカーに伝達する
|
||||
if (isNotificationColumn) {
|
||||
val list = ArrayList<TootNotification>()
|
||||
for (o in list_new) {
|
||||
if (o is TootNotification) {
|
||||
list.add(o)
|
||||
}
|
||||
}
|
||||
if (list.isNotEmpty()) {
|
||||
PollingWorker.injectData(context, access_info, list)
|
||||
}
|
||||
}
|
||||
|
||||
// 最新のIDをsince_idとして覚える(ソートはしない)
|
||||
var new_id_max: EntityId? = null
|
||||
var new_id_min: EntityId? = null
|
||||
for (o in list_new) {
|
||||
try {
|
||||
val id = o.getOrderId()
|
||||
if (id.toString().isEmpty()) continue
|
||||
if (new_id_max == null || id > new_id_max) new_id_max = id
|
||||
if (new_id_min == null || id < new_id_min) new_id_min = id
|
||||
} catch (ex: Throwable) {
|
||||
// IDを取得できないタイプのオブジェクトだった
|
||||
// ストリームに来るのは通知かステータスだから、多分ここは通らない
|
||||
log.trace(ex)
|
||||
}
|
||||
}
|
||||
|
||||
val tmpRecent = idRecent
|
||||
val tmpNewMax = new_id_max
|
||||
|
||||
if (tmpNewMax != null && (tmpRecent?.compareTo(tmpNewMax) ?: -1) == -1) {
|
||||
idRecent = tmpNewMax
|
||||
// XXX: コレはリフレッシュ時に取得漏れを引き起こすのでは…?
|
||||
// しかしコレなしだとリフレッシュ時に大量に読むことになる…
|
||||
}
|
||||
|
||||
val holder = viewHolder
|
||||
|
||||
// 事前にスクロール位置を覚えておく
|
||||
val holder_sp: ScrollPosition? = holder?.scrollPosition
|
||||
|
||||
// idx番目の要素がListViewの上端から何ピクセル下にあるか
|
||||
var restore_idx = -2
|
||||
var restore_y = 0
|
||||
if (holder != null) {
|
||||
if (list_data.size > 0) {
|
||||
try {
|
||||
restore_idx = holder.findFirstVisibleListItem()
|
||||
restore_y = holder.getListItemOffset(restore_idx)
|
||||
} catch (ex: IndexOutOfBoundsException) {
|
||||
restore_idx = -2
|
||||
restore_y = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 画面復帰時の自動リフレッシュではギャップが残る可能性がある
|
||||
if (bPutGap) {
|
||||
bPutGap = false
|
||||
try {
|
||||
if (list_data.size > 0 && new_id_min != null) {
|
||||
val since = list_data[0].getOrderId()
|
||||
if (new_id_min > since) {
|
||||
val gap = TootGap(new_id_min, since)
|
||||
list_new.add(gap)
|
||||
}
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
log.e(ex, "can't put gap.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
val changeList = ArrayList<AdapterChange>()
|
||||
|
||||
replaceConversationSummary(changeList, list_new, list_data)
|
||||
|
||||
val added = list_new.size // may 0
|
||||
|
||||
var doneSound = false
|
||||
for (o in list_new) {
|
||||
if (o is TootStatus) {
|
||||
o.highlightSound?.let {
|
||||
if (!doneSound) {
|
||||
doneSound = true
|
||||
App1.sound(it)
|
||||
}
|
||||
}
|
||||
o.highlightSpeech?.let {
|
||||
app_state.addSpeech(it.name, dedupMode = DedupMode.RecentExpire)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changeList.add(AdapterChange(AdapterChangeType.RangeInsert, 0, added))
|
||||
list_data.addAll(0, list_new)
|
||||
|
||||
fireShowContent(reason = "mergeStreamingMessage", changeList = changeList)
|
||||
|
||||
if (holder != null) {
|
||||
when {
|
||||
holder_sp == null -> {
|
||||
// スクロール位置が先頭なら先頭にする
|
||||
log.d("mergeStreamingMessage: has VH. missing scroll position.")
|
||||
viewHolder?.scrollToTop()
|
||||
}
|
||||
|
||||
holder_sp.isHead -> {
|
||||
// スクロール位置が先頭なら先頭にする
|
||||
log.d("mergeStreamingMessage: has VH. keep head. $holder_sp")
|
||||
holder.setScrollPosition(ScrollPosition())
|
||||
}
|
||||
|
||||
restore_idx < -1 -> {
|
||||
// 可視範囲の検出に失敗
|
||||
log.d("mergeStreamingMessage: has VH. can't get visible range.")
|
||||
}
|
||||
|
||||
else -> {
|
||||
// 現在の要素が表示され続けるようにしたい
|
||||
log.d("mergeStreamingMessage: has VH. added=$added")
|
||||
holder.setListItemTop(restore_idx + added, restore_y)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val scroll_save = this@Column.scroll_save
|
||||
when {
|
||||
|
||||
// スクロール位置が先頭なら先頭のまま
|
||||
scroll_save == null || scroll_save.isHead -> {
|
||||
|
||||
}
|
||||
|
||||
// 現在の要素が表示され続けるようにしたい
|
||||
else -> scroll_save.adapterIndex += added
|
||||
}
|
||||
}
|
||||
|
||||
updateMisskeyCapture()
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
internal constructor(
|
||||
app_state: AppState,
|
||||
access_info: SavedAccount,
|
||||
|
@ -746,13 +1136,6 @@ class Column(
|
|||
fun getColumnName(long: Boolean) =
|
||||
type.name2(this, long) ?: type.name1(context)
|
||||
|
||||
|
||||
|
||||
|
||||
private fun JsonObject.putIfTrue(key: String, value: Boolean) {
|
||||
if (value) put(key, true)
|
||||
}
|
||||
|
||||
@Throws(JsonException::class)
|
||||
fun encodeJSON(dst: JsonObject, old_index: Int) {
|
||||
dst[KEY_ACCOUNT_ROW_ID] = access_info.db_id
|
||||
|
@ -1164,11 +1547,7 @@ class Column(
|
|||
PollingWorker.queueNotificationCleared(context, access_info.db_id)
|
||||
}
|
||||
|
||||
val isNotificationColumn: Boolean
|
||||
get() = when (type) {
|
||||
ColumnType.NOTIFICATIONS, ColumnType.NOTIFICATION_FROM_ACCT -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
|
||||
fun removeNotificationOne(target_account: SavedAccount, notification: TootNotification) {
|
||||
if (!isNotificationColumn) return
|
||||
|
@ -1867,11 +2246,6 @@ class Column(
|
|||
env.update(client, parser)
|
||||
}
|
||||
|
||||
// DMカラム更新時に新APIの利用に成功したなら真
|
||||
internal var useConversationSummarys = false
|
||||
|
||||
// DMカラムのストリーミングイベントで新形式のイベントを利用できたなら真
|
||||
internal var useConversationSummaryStreaming = false
|
||||
|
||||
internal fun startLoading() {
|
||||
cancelLastTask()
|
||||
|
@ -2136,7 +2510,6 @@ class Column(
|
|||
return context.loadRawResource(res_id).decodeUTF8()
|
||||
}
|
||||
|
||||
private var cacheHeaderDesc: String? = null
|
||||
|
||||
fun getHeaderDesc(): String {
|
||||
var cache = cacheHeaderDesc
|
||||
|
@ -2209,10 +2582,10 @@ class Column(
|
|||
} else if (isSearchColumn) {
|
||||
// 検索カラムはリフレッシュもストリーミングもないが、表示開始のタイミングでリストの再描画を行いたい
|
||||
fireShowContent(reason = "Column onStart isSearchColumn", reset = true)
|
||||
} else {
|
||||
} else if( canStartStreaming() && streamSpec !=null ){
|
||||
// ギャップつきでストリーミング開始
|
||||
log.d("onStart: start streaming with gap.")
|
||||
resumeColumn(true)
|
||||
this.bPutGap = true
|
||||
fireShowColumnStatus()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2400,196 +2773,6 @@ class Column(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
val streamCallback = object : StreamCallback {
|
||||
|
||||
override fun onListeningStateChanged(status: StreamStatus) {
|
||||
if (is_dispose.get()) return
|
||||
|
||||
if (status == StreamStatus.Open) {
|
||||
updateMisskeyCapture()
|
||||
}
|
||||
|
||||
runOnMainLooper {
|
||||
if (is_dispose.get()) return@runOnMainLooper
|
||||
fireShowColumnStatus()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onTimelineItem(item: TimelineItem, channelId: String?,stream:JsonArray?) {
|
||||
if (is_dispose.get()) return
|
||||
|
||||
if (item is TootConversationSummary) {
|
||||
if (type != ColumnType.DIRECT_MESSAGES) return
|
||||
if (isFiltered(item.last_status)) return
|
||||
if (use_old_api) {
|
||||
useConversationSummaryStreaming = false
|
||||
return
|
||||
} else {
|
||||
useConversationSummaryStreaming = true
|
||||
}
|
||||
|
||||
} else if (item is TootNotification) {
|
||||
if (!isNotificationColumn) return
|
||||
if (isFiltered(item)) return
|
||||
|
||||
} else if (item is TootStatus) {
|
||||
if (isNotificationColumn) return
|
||||
|
||||
// マストドン2.6.0形式のDMカラム用イベントを利用したならば、その直後に発生する普通の投稿イベントを無視する
|
||||
if (useConversationSummaryStreaming) return
|
||||
|
||||
// マストドンはLTLに外部ユーザの投稿を表示しない
|
||||
if (type == ColumnType.LOCAL && isMastodon && item.account.isRemote) return
|
||||
|
||||
if (isFiltered(item)) return
|
||||
}
|
||||
|
||||
stream_data_queue.add(item)
|
||||
|
||||
app_state.handler.post(mergeStreamingMessage)
|
||||
}
|
||||
|
||||
|
||||
override fun onNoteUpdated(ev: MisskeyNoteUpdate, channelId: String?) {
|
||||
runOnMainLooper {
|
||||
if (is_dispose.get()) return@runOnMainLooper
|
||||
|
||||
// userId が自分かどうか調べる
|
||||
// アクセストークンの更新をして自分のuserIdが分かる状態でないとキャプチャ結果を反映させない
|
||||
// (でないとリアクションの2重カウントなどが発生してしまう)
|
||||
val myId = EntityId.from(access_info.token_info, TootApiClient.KEY_USER_ID)
|
||||
if (myId == null) {
|
||||
log.w("onNoteUpdated: missing my userId. updating access token is recommenced!!")
|
||||
}
|
||||
|
||||
val byMe = myId == ev.userId
|
||||
|
||||
val changeList = ArrayList<AdapterChange>()
|
||||
|
||||
fun scanStatus1(s: TootStatus?, idx: Int, block: (s: TootStatus) -> Boolean) {
|
||||
s ?: return
|
||||
if (s.id == ev.noteId) {
|
||||
if (block(s)) {
|
||||
changeList.add(AdapterChange(AdapterChangeType.RangeChange, idx, 1))
|
||||
}
|
||||
}
|
||||
scanStatus1(s.reblog, idx, block)
|
||||
scanStatus1(s.reply, idx, block)
|
||||
}
|
||||
|
||||
fun scanStatusAll(block: (s: TootStatus) -> Boolean) {
|
||||
for (i in 0 until list_data.size) {
|
||||
val o = list_data[i]
|
||||
if (o is TootStatus) {
|
||||
scanStatus1(o, i, block)
|
||||
} else if (o is TootNotification) {
|
||||
scanStatus1(o.status, i, block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (ev.type) {
|
||||
MisskeyNoteUpdate.Type.REACTION -> {
|
||||
scanStatusAll { s ->
|
||||
s.increaseReaction(ev.reaction, byMe, "onNoteUpdated ${ev.userId}")
|
||||
}
|
||||
}
|
||||
|
||||
MisskeyNoteUpdate.Type.UNREACTION -> {
|
||||
scanStatusAll { s ->
|
||||
s.decreaseReaction(ev.reaction, byMe, "onNoteUpdated ${ev.userId}")
|
||||
}
|
||||
}
|
||||
|
||||
MisskeyNoteUpdate.Type.VOTED -> {
|
||||
scanStatusAll { s ->
|
||||
s.enquete?.increaseVote(context, ev.choice, byMe) ?: false
|
||||
}
|
||||
}
|
||||
|
||||
MisskeyNoteUpdate.Type.DELETED -> {
|
||||
scanStatusAll { s ->
|
||||
s.markDeleted(context, ev.deletedAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changeList.isNotEmpty()) {
|
||||
fireShowContent(reason = "onNoteUpdated", changeList = changeList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAnnouncementUpdate(item: TootAnnouncement) {
|
||||
runOnMainLooper {
|
||||
val list = announcements
|
||||
if (list == null) {
|
||||
announcements = mutableListOf(item)
|
||||
} else {
|
||||
val index = list.indexOfFirst { it.id == item.id }
|
||||
list.add(
|
||||
0,
|
||||
if (index == -1) {
|
||||
item
|
||||
} else {
|
||||
TootAnnouncement.merge(list.removeAt(index), item)
|
||||
}
|
||||
)
|
||||
}
|
||||
announcementUpdated = SystemClock.elapsedRealtime()
|
||||
fireShowColumnHeader()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAnnouncementDelete(id: EntityId) {
|
||||
runOnMainLooper {
|
||||
val it = announcements?.iterator() ?: return@runOnMainLooper
|
||||
while (it.hasNext()) {
|
||||
val item = it.next()
|
||||
if (item.id == id) {
|
||||
it.remove()
|
||||
announcementUpdated = SystemClock.elapsedRealtime()
|
||||
fireShowColumnHeader()
|
||||
return@runOnMainLooper
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAnnouncementReaction(reaction: TootAnnouncement.Reaction) {
|
||||
runOnMainLooper {
|
||||
// find announcement
|
||||
val announcement_id = reaction.announcement_id ?: return@runOnMainLooper
|
||||
val announcement = announcements?.find { it.id == announcement_id } ?: return@runOnMainLooper
|
||||
|
||||
// find reaction
|
||||
val index = announcement.reactions?.indexOfFirst { it.name == reaction.name }
|
||||
when {
|
||||
reaction.count <= 0L -> {
|
||||
if (index != null && index != -1) announcement.reactions?.removeAt(index)
|
||||
}
|
||||
|
||||
index == null -> {
|
||||
announcement.reactions = ArrayList<TootAnnouncement.Reaction>().apply {
|
||||
add(reaction)
|
||||
}
|
||||
}
|
||||
|
||||
index == -1 -> announcement.reactions?.add(reaction)
|
||||
|
||||
else -> announcement.reactions?.get(index)?.let { old ->
|
||||
old.count = reaction.count
|
||||
// ストリーミングイベントにはmeが含まれないので、oldにあるmeは変更されない
|
||||
}
|
||||
}
|
||||
announcementUpdated = SystemClock.elapsedRealtime()
|
||||
fireShowColumnHeader()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 別スレッドから呼ばれるが大丈夫か
|
||||
fun canStartStreaming() = when {
|
||||
// 未初期化なら何もしない
|
||||
|
@ -2607,202 +2790,6 @@ class Column(
|
|||
else -> true
|
||||
}
|
||||
|
||||
private fun resumeColumn(bPutGap: Boolean) {
|
||||
|
||||
// カラム種別によってはストリーミングAPIを利用できない
|
||||
streamSpec ?: return
|
||||
|
||||
if (!canStartStreaming()) return
|
||||
|
||||
this.bPutGap = bPutGap
|
||||
|
||||
// TODO キューのクリアって必要? stream_data_queue.clear()
|
||||
|
||||
// streamReader = app_state.stream_reader.register(
|
||||
// access_info,
|
||||
// stream_path,
|
||||
// highlight_trie,
|
||||
// streamCallback
|
||||
// )
|
||||
fireShowColumnStatus()
|
||||
}
|
||||
|
||||
private val mergeStreamingMessage: Runnable = object : Runnable {
|
||||
override fun run() {
|
||||
|
||||
// 前回マージしてから暫くは待機する
|
||||
val handler = app_state.handler
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
val remain = last_show_stream_data.get() + 333L - now
|
||||
if (remain > 0) {
|
||||
handler.removeCallbacks(this)
|
||||
handler.postDelayed(this, remain)
|
||||
return
|
||||
}
|
||||
last_show_stream_data.set(now)
|
||||
|
||||
val tmpList = ArrayList<TimelineItem>()
|
||||
while (true) tmpList.add(stream_data_queue.poll() ?: break)
|
||||
if (tmpList.isEmpty()) return
|
||||
|
||||
// キューから読めた件数が0の場合を除き、少し後に再処理させることでマージ漏れを防ぐ
|
||||
handler.postDelayed(this, 333L)
|
||||
|
||||
// ストリーミングされるデータは全てID順に並んでいるはず
|
||||
tmpList.sortByDescending { it.getOrderId() }
|
||||
|
||||
val list_new = duplicate_map.filterDuplicate(tmpList)
|
||||
if (list_new.isEmpty()) return
|
||||
|
||||
for (item in list_new) {
|
||||
if (enable_speech && item is TootStatus) {
|
||||
app_state.addSpeech(item.reblog ?: item)
|
||||
}
|
||||
}
|
||||
|
||||
// 通知カラムならストリーミング経由で届いたデータを通知ワーカーに伝達する
|
||||
if (isNotificationColumn) {
|
||||
val list = ArrayList<TootNotification>()
|
||||
for (o in list_new) {
|
||||
if (o is TootNotification) {
|
||||
list.add(o)
|
||||
}
|
||||
}
|
||||
if (list.isNotEmpty()) {
|
||||
PollingWorker.injectData(context, access_info, list)
|
||||
}
|
||||
}
|
||||
|
||||
// 最新のIDをsince_idとして覚える(ソートはしない)
|
||||
var new_id_max: EntityId? = null
|
||||
var new_id_min: EntityId? = null
|
||||
for (o in list_new) {
|
||||
try {
|
||||
val id = o.getOrderId()
|
||||
if (id.toString().isEmpty()) continue
|
||||
if (new_id_max == null || id > new_id_max) new_id_max = id
|
||||
if (new_id_min == null || id < new_id_min) new_id_min = id
|
||||
} catch (ex: Throwable) {
|
||||
// IDを取得できないタイプのオブジェクトだった
|
||||
// ストリームに来るのは通知かステータスだから、多分ここは通らない
|
||||
log.trace(ex)
|
||||
}
|
||||
}
|
||||
|
||||
val tmpRecent = idRecent
|
||||
val tmpNewMax = new_id_max
|
||||
|
||||
if (tmpNewMax != null && (tmpRecent?.compareTo(tmpNewMax) ?: -1) == -1) {
|
||||
idRecent = tmpNewMax
|
||||
// XXX: コレはリフレッシュ時に取得漏れを引き起こすのでは…?
|
||||
// しかしコレなしだとリフレッシュ時に大量に読むことになる…
|
||||
}
|
||||
|
||||
val holder = viewHolder
|
||||
|
||||
// 事前にスクロール位置を覚えておく
|
||||
val holder_sp: ScrollPosition? = holder?.scrollPosition
|
||||
|
||||
// idx番目の要素がListViewの上端から何ピクセル下にあるか
|
||||
var restore_idx = -2
|
||||
var restore_y = 0
|
||||
if (holder != null) {
|
||||
if (list_data.size > 0) {
|
||||
try {
|
||||
restore_idx = holder.findFirstVisibleListItem()
|
||||
restore_y = holder.getListItemOffset(restore_idx)
|
||||
} catch (ex: IndexOutOfBoundsException) {
|
||||
restore_idx = -2
|
||||
restore_y = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 画面復帰時の自動リフレッシュではギャップが残る可能性がある
|
||||
if (bPutGap) {
|
||||
bPutGap = false
|
||||
try {
|
||||
if (list_data.size > 0 && new_id_min != null) {
|
||||
val since = list_data[0].getOrderId()
|
||||
if (new_id_min > since) {
|
||||
val gap = TootGap(new_id_min, since)
|
||||
list_new.add(gap)
|
||||
}
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
log.e(ex, "can't put gap.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
val changeList = ArrayList<AdapterChange>()
|
||||
|
||||
replaceConversationSummary(changeList, list_new, list_data)
|
||||
|
||||
val added = list_new.size // may 0
|
||||
|
||||
var doneSound = false
|
||||
for (o in list_new) {
|
||||
if (o is TootStatus) {
|
||||
o.highlightSound?.let {
|
||||
if (!doneSound) {
|
||||
doneSound = true
|
||||
App1.sound(it)
|
||||
}
|
||||
}
|
||||
o.highlightSpeech?.let {
|
||||
app_state.addSpeech(it.name, dedupMode = DedupMode.RecentExpire)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changeList.add(AdapterChange(AdapterChangeType.RangeInsert, 0, added))
|
||||
list_data.addAll(0, list_new)
|
||||
|
||||
fireShowContent(reason = "mergeStreamingMessage", changeList = changeList)
|
||||
|
||||
if (holder != null) {
|
||||
when {
|
||||
holder_sp == null -> {
|
||||
// スクロール位置が先頭なら先頭にする
|
||||
log.d("mergeStreamingMessage: has VH. missing scroll position.")
|
||||
viewHolder?.scrollToTop()
|
||||
}
|
||||
|
||||
holder_sp.isHead -> {
|
||||
// スクロール位置が先頭なら先頭にする
|
||||
log.d("mergeStreamingMessage: has VH. keep head. $holder_sp")
|
||||
holder.setScrollPosition(ScrollPosition())
|
||||
}
|
||||
|
||||
restore_idx < -1 -> {
|
||||
// 可視範囲の検出に失敗
|
||||
log.d("mergeStreamingMessage: has VH. can't get visible range.")
|
||||
}
|
||||
|
||||
else -> {
|
||||
// 現在の要素が表示され続けるようにしたい
|
||||
log.d("mergeStreamingMessage: has VH. added=$added")
|
||||
holder.setListItemTop(restore_idx + added, restore_y)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val scroll_save = this@Column.scroll_save
|
||||
when {
|
||||
|
||||
// スクロール位置が先頭なら先頭のまま
|
||||
scroll_save == null || scroll_save.isHead -> {
|
||||
|
||||
}
|
||||
|
||||
// 現在の要素が表示され続けるようにしたい
|
||||
else -> scroll_save.adapterIndex += added
|
||||
}
|
||||
}
|
||||
|
||||
updateMisskeyCapture()
|
||||
}
|
||||
}
|
||||
|
||||
private fun min(a: Int, b: Int): Int = if (a < b) a else b
|
||||
|
||||
|
@ -2986,11 +2973,6 @@ class Column(
|
|||
}
|
||||
}
|
||||
|
||||
val isMastodon: Boolean = access_info.isMastodon
|
||||
|
||||
val isMisskey: Boolean = access_info.isMisskey
|
||||
|
||||
val misskeyVersion = access_info.misskeyVersion
|
||||
|
||||
fun saveScrollPosition() {
|
||||
try {
|
||||
|
|
|
@ -70,7 +70,7 @@ enum class ColumnType(
|
|||
val iconId: (Acct) -> Int = unusedIconId,
|
||||
val name1: (context: Context) -> String = unusedName,
|
||||
val name2: Column.(long: Boolean) -> String? = unusedName2,
|
||||
val loading: suspend ColumnTask_Loading.(client: TootApiClient) -> TootApiResult?,
|
||||
val loading: suspend ColumnTask_Loading.(client: TootApiClient) -> TootApiResult?,
|
||||
val refresh: suspend ColumnTask_Refresh.(client: TootApiClient) -> TootApiResult? = unsupportedRefresh,
|
||||
val gap: suspend ColumnTask_Gap.(client: TootApiClient) -> TootApiResult? = unsupportedGap,
|
||||
val bAllowPseudo: Boolean = true,
|
||||
|
@ -78,8 +78,12 @@ enum class ColumnType(
|
|||
val bAllowMastodon: Boolean = true,
|
||||
val headerType: HeaderType? = null,
|
||||
val gapDirection: Column.(head: Boolean) -> Boolean = gapDirectionNone,
|
||||
val streamKeyMastodon: Column.()->JsonObject? = {null},
|
||||
val streamFilterMastodon: Column.(String?,TimelineItem)->Boolean = {_,_->true},
|
||||
val canAutoRefresh: Boolean = false,
|
||||
val streamKeyMastodon: Column.() -> JsonObject? = { null },
|
||||
val streamFilterMastodon: Column.(String?, TimelineItem) -> Boolean = { _, _ -> true },
|
||||
val streamNameMisskey: String? = null,
|
||||
val streamParamMisskey: Column.() -> JsonObject? = { null },
|
||||
val streamPathMisskey10: Column.() -> String? = { null },
|
||||
) {
|
||||
|
||||
ProfileStatusMastodon(
|
||||
|
@ -411,12 +415,18 @@ enum class ColumnType(
|
|||
gapDirection = gapDirectionBoth,
|
||||
bAllowPseudo = false,
|
||||
|
||||
canAutoRefresh = true,
|
||||
|
||||
streamKeyMastodon = {
|
||||
jsonObject(StreamSpec.STREAM to "user")
|
||||
},
|
||||
streamFilterMastodon = { stream,item->
|
||||
streamFilterMastodon = { stream, item ->
|
||||
item is TootStatus && (stream == null || stream == "user")
|
||||
}
|
||||
},
|
||||
|
||||
streamNameMisskey = "homeTimeline",
|
||||
streamParamMisskey = { null },
|
||||
streamPathMisskey10 = { "/" },
|
||||
),
|
||||
|
||||
LOCAL(
|
||||
|
@ -436,20 +446,25 @@ enum class ColumnType(
|
|||
},
|
||||
gapDirection = gapDirectionBoth,
|
||||
|
||||
canAutoRefresh = true,
|
||||
|
||||
streamKeyMastodon = {
|
||||
jsonObject(StreamSpec.STREAM to
|
||||
"public:local"
|
||||
.appendIf(":media", with_attachment)
|
||||
)
|
||||
},
|
||||
streamFilterMastodon = { stream,item->
|
||||
when{
|
||||
streamFilterMastodon = { stream, item ->
|
||||
when {
|
||||
item !is TootStatus -> false
|
||||
(stream != null && !stream.startsWith("public:local")) -> false
|
||||
with_attachment && item.media_attachments.isNullOrEmpty() -> false
|
||||
else->true
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
},
|
||||
streamNameMisskey = "localTimeline",
|
||||
streamParamMisskey = { null },
|
||||
streamPathMisskey10 = { "/local-timeline" },
|
||||
),
|
||||
|
||||
FEDERATE(
|
||||
|
@ -470,6 +485,8 @@ enum class ColumnType(
|
|||
},
|
||||
gapDirection = gapDirectionBoth,
|
||||
|
||||
canAutoRefresh = true,
|
||||
|
||||
streamKeyMastodon = {
|
||||
jsonObject(StreamSpec.STREAM to
|
||||
"public"
|
||||
|
@ -478,16 +495,20 @@ enum class ColumnType(
|
|||
)
|
||||
},
|
||||
|
||||
streamFilterMastodon = { stream,item->
|
||||
when{
|
||||
streamFilterMastodon = { stream, item ->
|
||||
when {
|
||||
item !is TootStatus -> false
|
||||
(stream != null && !stream.startsWith("public")) -> false
|
||||
(stream !=null && stream.contains(":local")) ->false
|
||||
(stream != null && stream.contains(":local")) -> false
|
||||
remote_only && item.account.acct == access_info.acct -> false
|
||||
with_attachment && item.media_attachments.isNullOrEmpty() -> false
|
||||
else->true
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
streamNameMisskey = "globalTimeline",
|
||||
streamParamMisskey = { null },
|
||||
streamPathMisskey10 = { "/global-timeline" },
|
||||
),
|
||||
|
||||
MISSKEY_HYBRID(
|
||||
|
@ -507,6 +528,12 @@ enum class ColumnType(
|
|||
)
|
||||
},
|
||||
gapDirection = gapDirectionBoth,
|
||||
|
||||
canAutoRefresh = true,
|
||||
|
||||
streamNameMisskey = "hybridTimeline",
|
||||
streamParamMisskey = { null },
|
||||
streamPathMisskey10 = { "/hybrid-timeline" },
|
||||
),
|
||||
|
||||
DOMAIN_TIMELINE(
|
||||
|
@ -531,6 +558,8 @@ enum class ColumnType(
|
|||
},
|
||||
gapDirection = gapDirectionBoth,
|
||||
|
||||
canAutoRefresh = true,
|
||||
|
||||
streamKeyMastodon = {
|
||||
jsonObject(StreamSpec.STREAM to
|
||||
"public:domain"
|
||||
|
@ -539,13 +568,13 @@ enum class ColumnType(
|
|||
)
|
||||
},
|
||||
|
||||
streamFilterMastodon = { stream,item->
|
||||
when{
|
||||
streamFilterMastodon = { stream, item ->
|
||||
when {
|
||||
item !is TootStatus -> false
|
||||
(stream != null && !stream.startsWith("public:domain")) -> false
|
||||
(stream != null && !stream.endsWith(instance_uri)) -> false
|
||||
with_attachment && item.media_attachments.isNullOrEmpty() -> false
|
||||
else->true
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
),
|
||||
|
@ -715,6 +744,8 @@ enum class ColumnType(
|
|||
|
||||
bAllowPseudo = false,
|
||||
|
||||
canAutoRefresh = true,
|
||||
|
||||
streamKeyMastodon = {
|
||||
jsonObject(StreamSpec.STREAM to "user")
|
||||
},
|
||||
|
@ -722,9 +753,13 @@ enum class ColumnType(
|
|||
when {
|
||||
item !is TootNotification -> false
|
||||
(stream != null && stream != "user") -> false
|
||||
else->true
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
streamNameMisskey = "main",
|
||||
streamParamMisskey = { null },
|
||||
streamPathMisskey10 = { "/" },
|
||||
),
|
||||
|
||||
NOTIFICATION_FROM_ACCT(
|
||||
|
@ -812,6 +847,8 @@ enum class ColumnType(
|
|||
},
|
||||
gapDirection = gapDirectionBoth,
|
||||
|
||||
canAutoRefresh = true,
|
||||
|
||||
streamKeyMastodon = {
|
||||
jsonObject(
|
||||
StreamSpec.STREAM to "hashtag".appendIf(":local", instance_local),
|
||||
|
@ -819,15 +856,20 @@ enum class ColumnType(
|
|||
)
|
||||
},
|
||||
|
||||
streamFilterMastodon = { stream,item->
|
||||
when{
|
||||
streamFilterMastodon = { stream, item ->
|
||||
when {
|
||||
item !is TootStatus -> false
|
||||
(stream != null && !stream.startsWith("hashtag")) -> false
|
||||
instance_local && (stream !=null && !stream.contains(":local")) ->false
|
||||
instance_local && (stream != null && !stream.contains(":local")) -> false
|
||||
|
||||
else-> this.checkHashtagExtra(item)
|
||||
else -> this.checkHashtagExtra(item)
|
||||
}
|
||||
}
|
||||
},
|
||||
streamNameMisskey = "hashtag",
|
||||
streamParamMisskey = { jsonObject ("q" to hashtag) },
|
||||
// Misskey10 というかめいすきーでタグTLのストリーミングができるのか不明
|
||||
// streamPathMisskey10 = { "/???? ?q=${hashtag.encodePercent()}" },
|
||||
|
||||
),
|
||||
|
||||
HASHTAG_FROM_ACCT(
|
||||
|
@ -1284,17 +1326,23 @@ enum class ColumnType(
|
|||
},
|
||||
gapDirection = gapDirectionBoth,
|
||||
|
||||
canAutoRefresh = true,
|
||||
|
||||
streamKeyMastodon = {
|
||||
jsonObject(StreamSpec.STREAM to "list", "list" to profile_id.toString())
|
||||
},
|
||||
|
||||
streamFilterMastodon = { stream,item->
|
||||
when{
|
||||
streamFilterMastodon = { stream, item ->
|
||||
when {
|
||||
item !is TootStatus -> false
|
||||
(stream != null && stream != "list:${profile_id}") -> false
|
||||
else-> true
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
},
|
||||
streamNameMisskey = "userList",
|
||||
streamParamMisskey = { jsonObject("listId" to profile_id.toString()) },
|
||||
streamPathMisskey10 = { "/user-list?listId=${profile_id.toString()}" },
|
||||
|
||||
),
|
||||
|
||||
LIST_MEMBER(21,
|
||||
|
@ -1393,15 +1441,17 @@ enum class ColumnType(
|
|||
bAllowPseudo = false,
|
||||
bAllowMisskey = false,
|
||||
|
||||
canAutoRefresh = true,
|
||||
|
||||
streamKeyMastodon = {
|
||||
jsonObject(StreamSpec.STREAM to "direct")
|
||||
},
|
||||
|
||||
streamFilterMastodon = { stream, _ ->
|
||||
when{
|
||||
when {
|
||||
(stream != null && stream != "direct") -> false
|
||||
|
||||
else-> true
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
),
|
||||
|
@ -1655,6 +1705,11 @@ enum class ColumnType(
|
|||
}
|
||||
},
|
||||
gapDirection = gapDirectionBoth,
|
||||
|
||||
canAutoRefresh = true,
|
||||
streamNameMisskey = "antenna",
|
||||
streamParamMisskey = { jsonObject("antennaId" to profile_id.toString()) },
|
||||
// Misskey10 にアンテナはない
|
||||
),
|
||||
|
||||
;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package jp.juggler.subwaytooter
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.util.LruCache
|
||||
import jp.juggler.subwaytooter.Column.Companion.READ_LIMIT
|
||||
import jp.juggler.subwaytooter.Column.Companion.log
|
||||
|
@ -12,6 +11,49 @@ import jp.juggler.subwaytooter.api.syncAccountByAcct
|
|||
import jp.juggler.util.*
|
||||
import java.util.*
|
||||
|
||||
val Column.isMastodon: Boolean
|
||||
get()= access_info.isMastodon
|
||||
|
||||
val Column.isMisskey: Boolean
|
||||
get()= access_info.isMisskey
|
||||
|
||||
val Column.misskeyVersion :Int
|
||||
get()= access_info.misskeyVersion
|
||||
|
||||
val Column.isSearchColumn: Boolean
|
||||
get() {
|
||||
return when (type) {
|
||||
ColumnType.SEARCH,
|
||||
ColumnType.SEARCH_MSP,
|
||||
ColumnType.SEARCH_TS,
|
||||
ColumnType.SEARCH_NOTESTOCK -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
val Column.isNotificationColumn: Boolean
|
||||
get() = when (type) {
|
||||
ColumnType.NOTIFICATIONS, ColumnType.NOTIFICATION_FROM_ACCT -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
// 公開ストリームなら真
|
||||
val Column.isPublicStream: Boolean
|
||||
get() {
|
||||
return when (type) {
|
||||
ColumnType.LOCAL,
|
||||
ColumnType.FEDERATE,
|
||||
ColumnType.HASHTAG,
|
||||
ColumnType.LOCAL_AROUND,
|
||||
ColumnType.FEDERATED_AROUND,
|
||||
ColumnType.DOMAIN_TIMELINE -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun Column.canAutoRefresh() =
|
||||
! access_info.isNA && type.canAutoRefresh
|
||||
|
||||
internal inline fun <reified T : TimelineItem> addAll(
|
||||
dstArg: ArrayList<TimelineItem>?,
|
||||
|
|
|
@ -18,9 +18,8 @@ class StreamDestination(
|
|||
val refCallback = WeakReference(column.streamCallback)
|
||||
|
||||
fun canStartStreaming(): Boolean {
|
||||
val column = refColumn.get()
|
||||
return when {
|
||||
column == null -> {
|
||||
return when (val column = refColumn.get()) {
|
||||
null -> {
|
||||
log.w("${spec.name} canStartStreaming: missing column.")
|
||||
false
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package jp.juggler.subwaytooter.streaming
|
||||
|
||||
import jp.juggler.subwaytooter.Column
|
||||
|
||||
// ストリーミング接続の状態
|
||||
enum class StreamStatus {
|
||||
Closed,
|
||||
|
@ -13,3 +15,7 @@ enum class StreamIndicatorState {
|
|||
REGISTERED, // registered, but not listening
|
||||
LISTENING,
|
||||
}
|
||||
|
||||
fun Column.getStreamingStatus() =
|
||||
app_state.streamManager.getStreamingStatus(access_info, internalId)
|
||||
?: StreamIndicatorState.NONE
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
package jp.juggler.subwaytooter.streaming
|
||||
|
||||
import jp.juggler.subwaytooter.Column
|
||||
import jp.juggler.subwaytooter.ColumnType
|
||||
import jp.juggler.subwaytooter.*
|
||||
import jp.juggler.subwaytooter.api.entity.TimelineItem
|
||||
import jp.juggler.subwaytooter.encodeQuery
|
||||
import jp.juggler.subwaytooter.makeHashtagQueryParams
|
||||
import jp.juggler.subwaytooter.streaming.StreamSpec.Companion.CHANNEL
|
||||
import jp.juggler.subwaytooter.streaming.StreamSpec.Companion.PARAMS
|
||||
import jp.juggler.subwaytooter.streaming.StreamSpec.Companion.STREAM
|
||||
|
@ -40,7 +37,7 @@ class StreamSpec(
|
|||
val params: JsonObject,
|
||||
val path: String,
|
||||
val name: String,
|
||||
val streamFilter: Column.(String?,TimelineItem)->Boolean = { _, _ -> true }
|
||||
val streamFilter: Column.(String?, TimelineItem) -> Boolean = { _, _ -> true }
|
||||
) {
|
||||
companion object {
|
||||
const val STREAM = "stream"
|
||||
|
@ -58,20 +55,10 @@ class StreamSpec(
|
|||
if (other is StreamSpec) return keyString == other.keyString
|
||||
return false
|
||||
}
|
||||
|
||||
fun match(stream: JsonArray): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private fun Column.streamKeyMastodon(): StreamSpec? {
|
||||
val root = type.streamKeyMastodon(this) ?: return null
|
||||
val filter = type.streamFilterMastodon
|
||||
|
||||
val path = "/api/v1/streaming/?${root.encodeQuery()}"
|
||||
|
||||
val sw = StringWriter()
|
||||
synchronized(sw.buffer) {
|
||||
private fun encodeStreamNameMastodon(root: JsonObject) = StringWriter()
|
||||
.also { sw ->
|
||||
sw.append(root.string(STREAM)!!)
|
||||
root.entries.sortedBy { it.key }.forEach { pair ->
|
||||
val (k, v) = pair
|
||||
|
@ -85,77 +72,23 @@ private fun Column.streamKeyMastodon(): StreamSpec? {
|
|||
sw.append(',').append(k).append('=').appendValue(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.toString()
|
||||
|
||||
return StreamSpec(root, path, sw.toString(),streamFilter=filter)
|
||||
|
||||
private fun Column.streamSpecMastodon(): StreamSpec? {
|
||||
|
||||
val root = type.streamKeyMastodon(this) ?: return null
|
||||
|
||||
return StreamSpec(
|
||||
params= root,
|
||||
path= "/api/v1/streaming/?${root.encodeQuery()}",
|
||||
name=encodeStreamNameMastodon(root),
|
||||
streamFilter = type.streamFilterMastodon
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
fun Column.streamKeyMisskey(): StreamSpec? {
|
||||
|
||||
// 使われ方は StreamConnection.subscribe を参照のこと
|
||||
fun x(channel: String, params: JsonObject = JsonObject()) =
|
||||
jsonObject(CHANNEL to channel, PARAMS to params)
|
||||
|
||||
val misskeyApiToken = access_info.misskeyApiToken
|
||||
|
||||
val root = when (misskeyApiToken) {
|
||||
null -> when (type) {
|
||||
ColumnType.LOCAL -> x("localTimeline")
|
||||
else -> null
|
||||
}
|
||||
|
||||
else -> when (type) {
|
||||
ColumnType.HOME ->
|
||||
x("homeTimeline")
|
||||
ColumnType.LOCAL ->
|
||||
x("localTimeline")
|
||||
ColumnType.MISSKEY_HYBRID ->
|
||||
x("hybridTimeline")
|
||||
ColumnType.FEDERATE ->
|
||||
x("globalTimeline")
|
||||
ColumnType.NOTIFICATIONS ->
|
||||
x("main")
|
||||
|
||||
ColumnType.MISSKEY_ANTENNA_TL ->
|
||||
x("antenna", jsonObject { put("antennaId", profile_id.toString()) })
|
||||
|
||||
ColumnType.LIST_TL ->
|
||||
x("userList", jsonObject { put("listId", profile_id.toString()) })
|
||||
|
||||
ColumnType.HASHTAG ->
|
||||
x("hashtag", jsonObject { put("q", hashtag) })
|
||||
|
||||
else -> null
|
||||
}
|
||||
} ?: return null
|
||||
|
||||
val path = when {
|
||||
// Misskey 11以降は統合されてる
|
||||
misskeyVersion >= 11 -> "/streaming"
|
||||
|
||||
// Misskey 10 認証なし
|
||||
// Misskey 8.25 からLTLだけ認証なしでも見れるようになった
|
||||
access_info.isPseudo -> when (type) {
|
||||
ColumnType.LOCAL -> "/local-timeline"
|
||||
else -> null
|
||||
}
|
||||
|
||||
// Misskey 10 認証あり
|
||||
// Misskey 8.25 からLTLだけ認証なしでも見れるようになった
|
||||
else -> when (type) {
|
||||
ColumnType.HOME, ColumnType.NOTIFICATIONS -> "/"
|
||||
ColumnType.LOCAL -> "/local-timeline"
|
||||
ColumnType.MISSKEY_HYBRID -> "/hybrid-timeline"
|
||||
ColumnType.FEDERATE -> "/global-timeline"
|
||||
ColumnType.LIST_TL -> "/user-list?listId=${profile_id.toString()}"
|
||||
// タグやアンテナには対応しない
|
||||
else -> null
|
||||
}
|
||||
} ?: return null
|
||||
|
||||
val sw = StringWriter()
|
||||
synchronized(sw.buffer) {
|
||||
private fun encodeStreamNameMisskey(root:JsonObject) =
|
||||
StringWriter().also{sw->
|
||||
sw.append(root.string(CHANNEL)!!)
|
||||
val params = root.jsonObject(PARAMS)!!
|
||||
params.entries.sortedBy { it.key }.forEach { pair ->
|
||||
|
@ -170,33 +103,41 @@ fun Column.streamKeyMisskey(): StreamSpec? {
|
|||
sw.append(',').append(k).append('=').appendValue(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.toString()
|
||||
|
||||
return StreamSpec(root, path, sw.toString())
|
||||
fun Column.streamSpecMisskey(): StreamSpec? {
|
||||
val channelName =
|
||||
if( access_info.misskeyApiToken==null && type != ColumnType.LOCAL) {
|
||||
null
|
||||
}else {
|
||||
type.streamNameMisskey
|
||||
} ?: return null
|
||||
|
||||
val path = when {
|
||||
// Misskey 11以降は統合されてる
|
||||
misskeyVersion >= 11 -> "/streaming"
|
||||
|
||||
else -> type.streamPathMisskey10(this)
|
||||
} ?: return null
|
||||
|
||||
val channelParam = type.streamParamMisskey(this) ?: JsonObject()
|
||||
val root = jsonObject(CHANNEL to channelName, PARAMS to channelParam)
|
||||
|
||||
return StreamSpec(
|
||||
params = root,
|
||||
path = path,
|
||||
name = encodeStreamNameMisskey(root),
|
||||
// no stream filter
|
||||
)
|
||||
}
|
||||
|
||||
// 公開ストリームなら真
|
||||
val Column.isPublicStream: Boolean
|
||||
get() {
|
||||
return when (type) {
|
||||
ColumnType.LOCAL,
|
||||
ColumnType.FEDERATE,
|
||||
ColumnType.HASHTAG,
|
||||
ColumnType.LOCAL_AROUND,
|
||||
ColumnType.FEDERATED_AROUND,
|
||||
ColumnType.DOMAIN_TIMELINE -> true
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
val Column.streamSpec: StreamSpec?
|
||||
get() = when {
|
||||
// 疑似アカウントではストリーミングAPIを利用できない
|
||||
// 2.1 では公開ストリームのみ利用できるらしい
|
||||
(access_info.isNA || access_info.isPseudo && !isPublicStream) -> null
|
||||
access_info.isMastodon -> streamKeyMastodon()
|
||||
access_info.isMisskey -> streamKeyMisskey()
|
||||
access_info.isMastodon -> streamSpecMastodon()
|
||||
access_info.isMisskey -> streamSpecMisskey()
|
||||
else -> null
|
||||
}
|
||||
|
||||
|
@ -207,4 +148,5 @@ fun Column.canStreaming() = when {
|
|||
else -> streamSpec != null
|
||||
}
|
||||
|
||||
fun Column.canAutoRefresh() = canStreaming()
|
||||
fun Column.canSpeech() =
|
||||
canStreaming() && !isNotificationColumn
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
package jp.juggler.subwaytooter.streaming
|
||||
|
||||
import jp.juggler.subwaytooter.Column
|
||||
|
||||
|
||||
fun Column.getStreamingStatus() =
|
||||
app_state.streamManager.getStreamingStatus(access_info, internalId)
|
||||
?: StreamIndicatorState.NONE
|
||||
|
||||
fun Column.canSpeech() =
|
||||
canStreaming() && !isNotificationColumn
|
|
@ -276,6 +276,10 @@ class JsonObject : LinkedHashMap<String, Any?>() {
|
|||
fun putNotNull(name: String, value: Any?) {
|
||||
if (value != null) put(name, value)
|
||||
}
|
||||
|
||||
fun putIfTrue(key: String, value: Boolean) {
|
||||
if (value) put(key, true)
|
||||
}
|
||||
}
|
||||
|
||||
class JsonTokenizer(reader: Reader) {
|
||||
|
|
|
@ -14,100 +14,100 @@ import java.util.regex.Matcher
|
|||
import java.util.regex.Pattern
|
||||
|
||||
object StringUtils {
|
||||
|
||||
val log = LogCategory("StringUtils")
|
||||
|
||||
val hexLower =
|
||||
charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f')
|
||||
|
||||
// BDI制御文字からその制御文字を閉じる文字を得るためのマップ
|
||||
val sanitizeBdiMap = HashMap<Char, Char>().apply {
|
||||
|
||||
val PDF = 0x202C.toChar() // Pop directional formatting (PDF)
|
||||
this[0x202A.toChar()] = PDF // Left-to-right embedding (LRE)
|
||||
this[0x202B.toChar()] = PDF // Right-to-left embedding (RLE)
|
||||
this[0x202D.toChar()] = PDF // Left-to-right override (LRO)
|
||||
this[0x202E.toChar()] = PDF // Right-to-left override (RLO)
|
||||
|
||||
val PDI = 0x2069.toChar() // Pop directional isolate (PDI)
|
||||
this[0x2066.toChar()] = PDI // Left-to-right isolate (LRI)
|
||||
this[0x2067.toChar()] = PDI // Right-to-left isolate (RLI)
|
||||
this[0x2068.toChar()] = PDI // First strong isolate (FSI)
|
||||
|
||||
// private const val ALM = 0x061c.toChar() // Arabic letter mark (ALM)
|
||||
// private const val LRM = 0x200E.toChar() // Left-to-right mark (LRM)
|
||||
// private const val RLM = 0x200F.toChar() // Right-to-left mark (RLM)
|
||||
}
|
||||
|
||||
|
||||
val log = LogCategory("StringUtils")
|
||||
|
||||
val hexLower =
|
||||
charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f')
|
||||
|
||||
// BDI制御文字からその制御文字を閉じる文字を得るためのマップ
|
||||
val sanitizeBdiMap = HashMap<Char, Char>().apply {
|
||||
|
||||
val PDF = 0x202C.toChar() // Pop directional formatting (PDF)
|
||||
this[0x202A.toChar()] = PDF // Left-to-right embedding (LRE)
|
||||
this[0x202B.toChar()] = PDF // Right-to-left embedding (RLE)
|
||||
this[0x202D.toChar()] = PDF // Left-to-right override (LRO)
|
||||
this[0x202E.toChar()] = PDF // Right-to-left override (RLO)
|
||||
|
||||
val PDI = 0x2069.toChar() // Pop directional isolate (PDI)
|
||||
this[0x2066.toChar()] = PDI // Left-to-right isolate (LRI)
|
||||
this[0x2067.toChar()] = PDI // Right-to-left isolate (RLI)
|
||||
this[0x2068.toChar()] = PDI // First strong isolate (FSI)
|
||||
|
||||
// private const val ALM = 0x061c.toChar() // Arabic letter mark (ALM)
|
||||
// private const val LRM = 0x200E.toChar() // Left-to-right mark (LRM)
|
||||
// private const val RLM = 0x200F.toChar() // Right-to-left mark (RLM)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// ByteArray
|
||||
|
||||
fun ByteArray.encodeBase64Url() : String =
|
||||
Base64.encodeToString(this, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
|
||||
fun ByteArray.encodeBase64Url(): String =
|
||||
Base64.encodeToString(this, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
|
||||
|
||||
fun ByteArray.digestSHA256() : ByteArray {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
digest.reset()
|
||||
return digest.digest(this)
|
||||
fun ByteArray.digestSHA256(): ByteArray {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
digest.reset()
|
||||
return digest.digest(this)
|
||||
}
|
||||
|
||||
fun ByteArray.startWith(
|
||||
key : ByteArray,
|
||||
thisOffset : Int = 0,
|
||||
keyOffset : Int = 0,
|
||||
length : Int = key.size - keyOffset
|
||||
) : Boolean {
|
||||
if(this.size - thisOffset >= length && key.size - keyOffset >= length) {
|
||||
for(i in 0 until length) {
|
||||
if(this[i + thisOffset] != key[i + keyOffset]) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
key: ByteArray,
|
||||
thisOffset: Int = 0,
|
||||
keyOffset: Int = 0,
|
||||
length: Int = key.size - keyOffset
|
||||
): Boolean {
|
||||
if (this.size - thisOffset >= length && key.size - keyOffset >= length) {
|
||||
for (i in 0 until length) {
|
||||
if (this[i + thisOffset] != key[i + keyOffset]) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 各要素の下位8ビットを使ってバイト配列を作る
|
||||
fun IntArray.toByteArray() : ByteArray {
|
||||
val dst = ByteArray(this.size)
|
||||
for(i in this.indices) {
|
||||
dst[i] = this[i].toByte()
|
||||
}
|
||||
return dst
|
||||
fun IntArray.toByteArray(): ByteArray {
|
||||
val dst = ByteArray(this.size)
|
||||
for (i in this.indices) {
|
||||
dst[i] = this[i].toByte()
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// 各要素の下位8ビットを使ってバイト配列を作る
|
||||
fun CharArray.toLowerByteArray() : ByteArray {
|
||||
val dst = ByteArray(this.size)
|
||||
for(i in this.indices) {
|
||||
dst[i] = this[i].toByte()
|
||||
}
|
||||
return dst
|
||||
fun CharArray.toLowerByteArray(): ByteArray {
|
||||
val dst = ByteArray(this.size)
|
||||
for (i in this.indices) {
|
||||
dst[i] = this[i].toByte()
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// CharSequence
|
||||
|
||||
fun CharSequence.replaceFirst(pattern : Pattern, replacement : String) : String =
|
||||
pattern.matcher(this).replaceFirst(replacement)
|
||||
fun CharSequence.replaceFirst(pattern: Pattern, replacement: String): String =
|
||||
pattern.matcher(this).replaceFirst(replacement)
|
||||
|
||||
fun CharSequence.replaceAll(pattern : Pattern, replacement : String) : String =
|
||||
pattern.matcher(this).replaceAll(replacement)
|
||||
fun CharSequence.replaceAll(pattern: Pattern, replacement: String): String =
|
||||
pattern.matcher(this).replaceAll(replacement)
|
||||
|
||||
// %1$s を含む文字列リソースを利用して装飾テキストの前後に文字列を追加する
|
||||
fun CharSequence?.intoStringResource(context : Context, string_id : Int) : Spannable {
|
||||
|
||||
val s = context.getString(string_id)
|
||||
val end = s.length
|
||||
val pos = s.indexOf("%1\$s")
|
||||
if(pos == - 1) return SpannableString(s)
|
||||
|
||||
val sb = SpannableStringBuilder()
|
||||
if(pos > 0) sb.append(s.substring(0, pos))
|
||||
if(this != null) sb.append(this)
|
||||
if(pos + 4 < end) sb.append(s.substring(pos + 4, end))
|
||||
return sb
|
||||
fun CharSequence?.intoStringResource(context: Context, string_id: Int): Spannable {
|
||||
|
||||
val s = context.getString(string_id)
|
||||
val end = s.length
|
||||
val pos = s.indexOf("%1\$s")
|
||||
if (pos == -1) return SpannableString(s)
|
||||
|
||||
val sb = SpannableStringBuilder()
|
||||
if (pos > 0) sb.append(s.substring(0, pos))
|
||||
if (this != null) sb.append(this)
|
||||
if (pos + 4 < end) sb.append(s.substring(pos + 4, end))
|
||||
return sb
|
||||
}
|
||||
|
||||
//fun Char.hex2int() : Int {
|
||||
|
@ -117,41 +117,41 @@ fun CharSequence?.intoStringResource(context : Context, string_id : Int) : Spann
|
|||
// return 0
|
||||
//}
|
||||
|
||||
fun CharSequence.codePointBefore(index : Int) : Int {
|
||||
if(index > 0) {
|
||||
val c2 = this[index - 1]
|
||||
if(Character.isLowSurrogate(c2) && index > 1) {
|
||||
val c1 = this[index - 2]
|
||||
if(Character.isHighSurrogate(c1)) return Character.toCodePoint(c1, c2)
|
||||
}
|
||||
return c2.toInt()
|
||||
} else {
|
||||
return - 1
|
||||
}
|
||||
fun CharSequence.codePointBefore(index: Int): Int {
|
||||
if (index > 0) {
|
||||
val c2 = this[index - 1]
|
||||
if (Character.isLowSurrogate(c2) && index > 1) {
|
||||
val c1 = this[index - 2]
|
||||
if (Character.isHighSurrogate(c1)) return Character.toCodePoint(c1, c2)
|
||||
}
|
||||
return c2.toInt()
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <S : CharSequence, Z : Any?> S?.letNotEmpty(block : (S) -> Z?) : Z? =
|
||||
if(this?.isNotEmpty() == true) {
|
||||
block(this)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
inline fun <S : CharSequence, Z : Any?> S?.letNotEmpty(block: (S) -> Z?): Z? =
|
||||
if (this?.isNotEmpty() == true) {
|
||||
block(this)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// usage: str.notEmpty() ?: fallback
|
||||
// equivalent: if(this.isNotEmpty() ) this else null
|
||||
fun <S : CharSequence> S?.notEmpty() : S? = if(this?.isNotEmpty() == true) this else null
|
||||
fun <S : CharSequence> S?.notEmpty(): S? = if (this?.isNotEmpty() == true) this else null
|
||||
|
||||
fun <S : CharSequence> S?.notBlank() : S? = if(this?.isNotBlank() == true) this else null
|
||||
fun <S : CharSequence> S?.notBlank(): S? = if (this?.isNotBlank() == true) this else null
|
||||
|
||||
fun CharSequence.toUri() : Uri = Uri.parse(toString())
|
||||
fun CharSequence.toUri(): Uri = Uri.parse(toString())
|
||||
|
||||
fun CharSequence?.mayUri() : Uri? = try {
|
||||
if(this?.isNotEmpty() == true)
|
||||
toUri()
|
||||
else
|
||||
null
|
||||
} catch(ignored : Throwable) {
|
||||
null
|
||||
fun CharSequence?.mayUri(): Uri? = try {
|
||||
if (this?.isNotEmpty() == true)
|
||||
toUri()
|
||||
else
|
||||
null
|
||||
} catch (ignored: Throwable) {
|
||||
null
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
|
@ -166,52 +166,52 @@ fun String.encodeUTF8() = this.toByteArray(charsetUTF8)
|
|||
|
||||
fun ByteArray.decodeUTF8() = this.toString(charsetUTF8)
|
||||
|
||||
fun String.codePointCount(beginIndex : Int = 0) : Int = this.codePointCount(beginIndex, this.length)
|
||||
fun String.codePointCount(beginIndex: Int = 0): Int = this.codePointCount(beginIndex, this.length)
|
||||
|
||||
// 16進ダンプ
|
||||
fun ByteArray.encodeHex() : String {
|
||||
val sb = StringBuilder()
|
||||
for(b in this) {
|
||||
sb.appendHex2(b.toInt())
|
||||
}
|
||||
return sb.toString()
|
||||
fun ByteArray.encodeHex(): String {
|
||||
val sb = StringBuilder()
|
||||
for (b in this) {
|
||||
sb.appendHex2(b.toInt())
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
fun StringBuilder.appendHex2(value : Int) : StringBuilder {
|
||||
this.append(StringUtils.hexLower[(value shr 4) and 15])
|
||||
this.append(StringUtils.hexLower[value and 15])
|
||||
return this
|
||||
fun StringBuilder.appendHex2(value: Int): StringBuilder {
|
||||
this.append(StringUtils.hexLower[(value shr 4) and 15])
|
||||
this.append(StringUtils.hexLower[value and 15])
|
||||
return this
|
||||
}
|
||||
|
||||
fun ByteArray.encodeHexLower() : String {
|
||||
val size = this.size
|
||||
val sb = StringBuilder(size * 2)
|
||||
for(i in 0 until size) {
|
||||
val value = this[i].toInt()
|
||||
sb.append(StringUtils.hexLower[(value shr 4) and 15])
|
||||
sb.append(StringUtils.hexLower[value and 15])
|
||||
}
|
||||
return sb.toString()
|
||||
fun ByteArray.encodeHexLower(): String {
|
||||
val size = this.size
|
||||
val sb = StringBuilder(size * 2)
|
||||
for (i in 0 until size) {
|
||||
val value = this[i].toInt()
|
||||
sb.append(StringUtils.hexLower[(value shr 4) and 15])
|
||||
sb.append(StringUtils.hexLower[value and 15])
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
fun String?.optInt() : Int? {
|
||||
return try {
|
||||
this?.toInt(10)
|
||||
} catch(ignored : Throwable) {
|
||||
null
|
||||
}
|
||||
fun String?.optInt(): Int? {
|
||||
return try {
|
||||
this?.toInt(10)
|
||||
} catch (ignored: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun String?.filterNotEmpty() : String? = when {
|
||||
this == null -> null
|
||||
this.isEmpty() -> null
|
||||
else -> this
|
||||
fun String?.filterNotEmpty(): String? = when {
|
||||
this == null -> null
|
||||
this.isEmpty() -> null
|
||||
else -> this
|
||||
}
|
||||
|
||||
fun String?.ellipsizeDot3(max : Int) = when {
|
||||
this == null -> null
|
||||
this.length > max -> this.substring(0, max - 1) + "…"
|
||||
else -> this
|
||||
fun String?.ellipsizeDot3(max: Int) = when {
|
||||
this == null -> null
|
||||
this.length > max -> this.substring(0, max - 1) + "…"
|
||||
else -> this
|
||||
}
|
||||
|
||||
//fun String.toCamelCase() : String {
|
||||
|
@ -224,37 +224,37 @@ fun String?.ellipsizeDot3(max : Int) = when {
|
|||
// return sb.toString()
|
||||
//}
|
||||
|
||||
fun ellipsize(src : String, limit : Int) : String =
|
||||
if(src.codePointCount(0, src.length) <= limit) {
|
||||
src
|
||||
} else {
|
||||
"${src.substring(0, src.offsetByCodePoints(0, limit))}…"
|
||||
}
|
||||
fun ellipsize(src: String, limit: Int): String =
|
||||
if (src.codePointCount(0, src.length) <= limit) {
|
||||
src
|
||||
} else {
|
||||
"${src.substring(0, src.offsetByCodePoints(0, limit))}…"
|
||||
}
|
||||
|
||||
fun String.sanitizeBDI() : String {
|
||||
|
||||
// 文字列をスキャンしてBDI制御文字をスタックに入れていく
|
||||
var stack : LinkedList<Char>? = null
|
||||
for(c in this) {
|
||||
val closer = StringUtils.sanitizeBdiMap[c]
|
||||
if(closer != null) {
|
||||
if(stack == null) stack = LinkedList()
|
||||
stack.add(closer)
|
||||
} else if(stack?.isNotEmpty() == true && stack.last == c) {
|
||||
stack.removeLast()
|
||||
}
|
||||
}
|
||||
|
||||
if(stack?.isNotEmpty() == true) {
|
||||
val sb = StringBuilder(this.length + stack.size)
|
||||
sb.append(this)
|
||||
while(! stack.isEmpty()) {
|
||||
sb.append(stack.removeLast())
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
return this
|
||||
fun String.sanitizeBDI(): String {
|
||||
|
||||
// 文字列をスキャンしてBDI制御文字をスタックに入れていく
|
||||
var stack: LinkedList<Char>? = null
|
||||
for (c in this) {
|
||||
val closer = StringUtils.sanitizeBdiMap[c]
|
||||
if (closer != null) {
|
||||
if (stack == null) stack = LinkedList()
|
||||
stack.add(closer)
|
||||
} else if (stack?.isNotEmpty() == true && stack.last == c) {
|
||||
stack.removeLast()
|
||||
}
|
||||
}
|
||||
|
||||
if (stack?.isNotEmpty() == true) {
|
||||
val sb = StringBuilder(this.length + stack.size)
|
||||
sb.append(this)
|
||||
while (!stack.isEmpty()) {
|
||||
sb.append(stack.removeLast())
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
//fun String.dumpCodePoints() : CharSequence {
|
||||
|
@ -284,36 +284,36 @@ fun String.sanitizeBDI() : String {
|
|||
// return md.digest(this.encodeUTF8()).encodeHex()
|
||||
//}
|
||||
|
||||
fun String.digestSHA256Hex() : String {
|
||||
return this.encodeUTF8().digestSHA256().encodeHex()
|
||||
fun String.digestSHA256Hex(): String {
|
||||
return this.encodeUTF8().digestSHA256().encodeHex()
|
||||
}
|
||||
|
||||
fun String.digestSHA256Base64Url() : String {
|
||||
return this.encodeUTF8().digestSHA256().encodeBase64Url()
|
||||
fun String.digestSHA256Base64Url(): String {
|
||||
return this.encodeUTF8().digestSHA256().encodeBase64Url()
|
||||
}
|
||||
|
||||
// Uri.encode(s:Nullable) だと nullチェックができないので、簡単なラッパーを用意する
|
||||
fun String.encodePercent(allow : String? = null) : String = Uri.encode(this, allow)
|
||||
fun String.encodePercent(allow: String? = null): String = Uri.encode(this, allow)
|
||||
|
||||
// replace + to %20, then decode it.
|
||||
fun String.decodePercent() : String =
|
||||
Uri.decode(replace("+", "%20"))
|
||||
fun String.decodePercent(): String =
|
||||
Uri.decode(replace("+", "%20"))
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Throwable
|
||||
|
||||
fun Throwable.withCaption(fmt : String?, vararg args : Any) =
|
||||
"${
|
||||
if(fmt == null || args.isEmpty())
|
||||
fmt
|
||||
else
|
||||
String.format(fmt, *args)
|
||||
}: ${this.javaClass.simpleName} ${this.message}"
|
||||
fun Throwable.withCaption(fmt: String?, vararg args: Any) =
|
||||
"${
|
||||
if (fmt == null || args.isEmpty())
|
||||
fmt
|
||||
else
|
||||
String.format(fmt, *args)
|
||||
}: ${this.javaClass.simpleName} ${this.message}"
|
||||
|
||||
fun Throwable.withCaption(resources : Resources, string_id : Int, vararg args : Any) =
|
||||
"${
|
||||
resources.getString(string_id, *args)
|
||||
}: ${this.javaClass.simpleName} ${this.message}"
|
||||
fun Throwable.withCaption(resources: Resources, string_id: Int, vararg args: Any) =
|
||||
"${
|
||||
resources.getString(string_id, *args)
|
||||
}: ${this.javaClass.simpleName} ${this.message}"
|
||||
|
||||
////////////////////////////////////////////////////////////////////
|
||||
// Bundle
|
||||
|
@ -329,12 +329,12 @@ fun Throwable.withCaption(resources : Resources, string_id : Int, vararg args :
|
|||
////////////////////////////////////////////////////////////////
|
||||
// Pattern
|
||||
|
||||
fun Matcher.groupEx(g : Int) : String? =
|
||||
try {
|
||||
group(g)
|
||||
} catch(ex : Throwable) {
|
||||
null
|
||||
}
|
||||
fun Matcher.groupEx(g: Int): String? =
|
||||
try {
|
||||
group(g)
|
||||
} catch (ex: Throwable) {
|
||||
null
|
||||
}
|
||||
|
||||
// make Array<String> to HashSet<String>
|
||||
fun <T> Array<T>.toHashSet() = HashSet<T>().also { it.addAll(this) }
|
||||
|
@ -342,12 +342,12 @@ fun <T> Array<T>.toHashSet() = HashSet<T>().also { it.addAll(this) }
|
|||
//fun <T> Iterable<T>.toHashSet() = HashSet<T>().also { it.addAll(this) }
|
||||
//fun <T> Sequence<T>.toHashSet() = HashSet<T>().also { it.addAll(this) }
|
||||
|
||||
fun defaultLocale(context : Context) : Locale =
|
||||
if(Build.VERSION.SDK_INT >= 24) {
|
||||
context.resources.configuration.locales[0]
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
context.resources.configuration.locale
|
||||
}
|
||||
fun defaultLocale(context: Context): Locale =
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
context.resources.configuration.locales[0]
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
context.resources.configuration.locale
|
||||
}
|
||||
|
||||
fun Matcher.findOrNull() = if(find()) this else null
|
||||
fun Matcher.findOrNull() = if (find()) this else null
|
Loading…
Reference in New Issue