1194 lines
44 KiB
Kotlin
1194 lines
44 KiB
Kotlin
package jp.juggler.subwaytooter.column
|
|
|
|
import android.os.SystemClock
|
|
import jp.juggler.subwaytooter.App1
|
|
import jp.juggler.subwaytooter.DedupMode
|
|
import jp.juggler.subwaytooter.R
|
|
import jp.juggler.subwaytooter.api.*
|
|
import jp.juggler.subwaytooter.api.entity.*
|
|
import jp.juggler.subwaytooter.api.finder.*
|
|
import jp.juggler.subwaytooter.columnviewholder.setScrollPosition
|
|
import jp.juggler.subwaytooter.pref.PrefB
|
|
import jp.juggler.subwaytooter.pref.PrefI
|
|
import jp.juggler.subwaytooter.util.ScrollPosition
|
|
import jp.juggler.util.*
|
|
import jp.juggler.util.coroutine.runOnMainLooper
|
|
import jp.juggler.util.coroutine.runOnMainLooperDelayed
|
|
import jp.juggler.util.data.JsonArray
|
|
import jp.juggler.util.data.JsonObject
|
|
import jp.juggler.util.data.notEmpty
|
|
import jp.juggler.util.log.LogCategory
|
|
import jp.juggler.util.log.withCaption
|
|
import jp.juggler.util.network.toPostRequestBuilder
|
|
import jp.juggler.util.ui.AdapterChange
|
|
import jp.juggler.util.ui.AdapterChangeType
|
|
|
|
@Suppress("ClassNaming")
|
|
class ColumnTask_Refresh(
|
|
columnArg: Column,
|
|
private val bSilent: Boolean,
|
|
val bBottom: Boolean,
|
|
internal val postedStatusId: EntityId? = null,
|
|
private val refreshAfterToot: Int = -1,
|
|
) : ColumnTask(
|
|
columnArg,
|
|
if (bBottom) ColumnTaskType.REFRESH_BOTTOM else ColumnTaskType.REFRESH_TOP
|
|
) {
|
|
|
|
companion object {
|
|
private val log = LogCategory("CT_Refresh")
|
|
}
|
|
|
|
private var filterUpdated = false
|
|
|
|
override suspend fun background(): TootApiResult? {
|
|
ctStarted.set(true)
|
|
|
|
val client = TootApiClient(context, callback = object : TootApiCallback {
|
|
override suspend fun isApiCancelled() = isCancelled || column.isDispose.get()
|
|
|
|
override suspend fun publishApiProgress(s: String) {
|
|
runOnMainLooper {
|
|
if (isCancelled) return@runOnMainLooper
|
|
column.taskProgress = s
|
|
column.fireShowContent(reason = "refresh progress", changeList = ArrayList())
|
|
}
|
|
}
|
|
})
|
|
client.account = accessInfo
|
|
try {
|
|
|
|
if (!bBottom) {
|
|
val filterList = column.loadFilter2(client)
|
|
if (filterList != null) {
|
|
column.keywordFilterTrees = column.encodeFilterTree(filterList)
|
|
filterUpdated = true
|
|
}
|
|
}
|
|
|
|
return column.type.refresh(this, client)
|
|
} catch (ex: Throwable) {
|
|
return TootApiResult(ex.withCaption("refresh failed."))
|
|
} finally {
|
|
try {
|
|
column.updateRelation(client, listTmp, column.whoAccount, parser)
|
|
} catch (ex: Throwable) {
|
|
log.e(ex, "updateRelation failed.")
|
|
}
|
|
ctClosed.set(true)
|
|
runOnMainLooperDelayed(333L) {
|
|
if (!isCancelled) column.fireShowColumnStatus()
|
|
}
|
|
}
|
|
}
|
|
|
|
override suspend fun handleResult(result: TootApiResult?) {
|
|
if (column.isDispose.get()) return
|
|
|
|
if (isCancelled || result == null) {
|
|
return
|
|
}
|
|
try {
|
|
column.lastTask = null
|
|
column.bRefreshLoading = false
|
|
|
|
if (filterUpdated) {
|
|
column.checkFiltersForListData(column.keywordFilterTrees)
|
|
}
|
|
|
|
val error = result.error
|
|
if (error != null) {
|
|
column.mRefreshLoadingError = error
|
|
column.mRefreshLoadingErrorTime = SystemClock.elapsedRealtime()
|
|
column.fireShowContent(reason = "refresh error", changeList = ArrayList())
|
|
return
|
|
}
|
|
|
|
val listNew = when (column.type) {
|
|
// 編集履歴は投稿日時で重複排除する
|
|
ColumnType.STATUS_HISTORY -> column.duplicateMap.filterDuplicateByCreatedAt(listTmp)
|
|
|
|
else -> column.duplicateMap.filterDuplicate(listTmp)
|
|
}
|
|
if (listNew.isEmpty()) {
|
|
column.fireShowContent(
|
|
reason = "refresh list_new is empty",
|
|
changeList = ArrayList()
|
|
)
|
|
return
|
|
}
|
|
|
|
// 事前にスクロール位置を覚えておく
|
|
var sp: ScrollPosition? = null
|
|
val holder = column.viewHolder
|
|
if (holder != null) {
|
|
sp = holder.scrollPosition
|
|
}
|
|
|
|
if (bBottom) {
|
|
val changeList = listOf(
|
|
AdapterChange(
|
|
AdapterChangeType.RangeInsert,
|
|
column.listData.size,
|
|
listNew.size
|
|
)
|
|
)
|
|
column.listData.addAll(listNew)
|
|
column.fireShowContent(reason = "refresh updated bottom", changeList = changeList)
|
|
|
|
// 新着が少しだけ見えるようにスクロール位置を移動する
|
|
if (sp != null) {
|
|
holder?.setScrollPosition(sp, 20f)
|
|
}
|
|
} else {
|
|
|
|
val changeList = ArrayList<AdapterChange>()
|
|
|
|
if (column.listData.isNotEmpty() && column.listData[0] is TootGap) {
|
|
changeList.add(AdapterChange(AdapterChangeType.RangeRemove, 0, 1))
|
|
column.listData.removeAt(0)
|
|
}
|
|
|
|
var doneSound = false
|
|
for (o in listNew) {
|
|
if (o is TootStatus) {
|
|
o.highlightSound?.let {
|
|
if (!doneSound) {
|
|
doneSound = true
|
|
App1.sound(it)
|
|
}
|
|
}
|
|
o.highlightSpeech?.name.notEmpty()?.let {
|
|
column.appState.addSpeech(it, dedupMode = DedupMode.RecentExpire)
|
|
}
|
|
}
|
|
}
|
|
|
|
replaceConversationSummary(changeList, listNew, column.listData)
|
|
|
|
val added = listNew.size // may 0
|
|
|
|
// 投稿後のリフレッシュなら当該投稿の位置を探す
|
|
var statusIndex = -1
|
|
for (i in 0 until added) {
|
|
val o = listNew[i]
|
|
if (o is TootStatus && o.id == postedStatusId) {
|
|
statusIndex = i
|
|
break
|
|
}
|
|
}
|
|
|
|
changeList.add(AdapterChange(AdapterChangeType.RangeInsert, 0, added))
|
|
column.listData.addAll(0, listNew)
|
|
column.fireShowContent(reason = "refresh updated head", changeList = changeList)
|
|
|
|
if (statusIndex >= 0 && refreshAfterToot == PrefI.RAT_REFRESH_SCROLL) {
|
|
// 投稿後にその投稿にスクロールする
|
|
if (holder != null) {
|
|
holder.setScrollPosition(
|
|
ScrollPosition(column.toAdapterIndex(statusIndex)),
|
|
0f
|
|
)
|
|
} else {
|
|
column.scrollSave = ScrollPosition(column.toAdapterIndex(statusIndex))
|
|
}
|
|
} else {
|
|
//
|
|
val scrollSave = column.scrollSave
|
|
when {
|
|
// ViewHolderがある場合は増加件数分+deltaの位置にスクロールする
|
|
sp != null -> {
|
|
sp.adapterIndex += added
|
|
val delta = if (bSilent) 0f else -20f
|
|
holder?.setScrollPosition(sp, delta)
|
|
}
|
|
// ViewHolderがなくて保存中の位置がある場合、増加件数分ずらす。deltaは難しいので反映しない
|
|
scrollSave != null -> scrollSave.adapterIndex += added
|
|
// 保存中の位置がない場合、保存中の位置を新しく作る
|
|
else -> column.scrollSave =
|
|
ScrollPosition(column.toAdapterIndex(added))
|
|
}
|
|
}
|
|
}
|
|
|
|
column.updateMisskeyCapture()
|
|
} finally {
|
|
column.fireShowColumnStatus()
|
|
|
|
if (!bBottom) {
|
|
column.bRefreshingTop = false
|
|
|
|
// TODO ロード状態の度にストリーミングを止めるのはやめる
|
|
// TODO ストリーミングから受信したデータの反映をロード中は行わないようにしたい
|
|
// column.resumeStreaming(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
private suspend fun <T : TimelineItem> refreshTopMisskey(
|
|
logCaption: String,
|
|
requester: suspend (first: Boolean) -> TootApiResult?,
|
|
arrayFinder: (JsonObject) -> JsonArray?,
|
|
listParser: (parser: TootParser, jsonArray: JsonArray) -> List<T>,
|
|
adder: (List<T>, Boolean) -> Unit,
|
|
): TootApiResult? {
|
|
|
|
// ColumnPagingType.Defaultだけが始端を更新できる
|
|
if (column.pagingType != ColumnPagingType.Default) {
|
|
return TootApiResult("can't refresh top.")
|
|
}
|
|
|
|
val addToHead = true
|
|
|
|
// misskeyの場合、sinceIdを指定したら未読範囲の古い方から読んでしまう
|
|
// 最新まで読めるとは限らない
|
|
// 先頭にギャップを置くかもしれない
|
|
var willAddGap = false
|
|
|
|
fun parseResult(result: TootApiResult?): Boolean {
|
|
val first = listTmp?.isEmpty() != false
|
|
|
|
if (result == null) {
|
|
if (!first) willAddGap = true
|
|
return log.d("$logCaption:cancelled.")
|
|
}
|
|
|
|
result.jsonObject?.let { result.data = arrayFinder(it) }
|
|
|
|
val array = result.jsonArray
|
|
if (array == null) {
|
|
if (!first) willAddGap = true
|
|
return log.w("$logCaption: missing item list")
|
|
}
|
|
|
|
val src = listParser(parser, array)
|
|
if (listTmp == null) listTmp = ArrayList(src.size)
|
|
adder(src, addToHead)
|
|
|
|
column.saveRangeTop(result = result, list = src)
|
|
|
|
return when {
|
|
// より新しいデータがあるかどうかはわからない。
|
|
// カラのデータを読めたら終端とする
|
|
src.isEmpty() -> log.d("$logCaption: empty item list")
|
|
else -> true
|
|
}
|
|
}
|
|
|
|
val timeStart = SystemClock.elapsedRealtime()
|
|
|
|
// 初回のリクエスト
|
|
val firstResult = requester(true)
|
|
var more = parseResult(firstResult)
|
|
|
|
// 頭の方を読む時は隙間を減らすため、フィルタの有無に関係なく繰り返しを行う
|
|
while (more) more = when {
|
|
isCancelled ->
|
|
log.d("$logCaption: cancelled.")
|
|
|
|
(listTmp?.size ?: 0) >= Column.LOOP_READ_ENOUGH -> {
|
|
// 既に十分読んだなら止める
|
|
willAddGap = true
|
|
log.d("$logCaption: read enough. make gap.")
|
|
}
|
|
|
|
SystemClock.elapsedRealtime() - timeStart > Column.LOOP_TIMEOUT -> {
|
|
willAddGap = true
|
|
log.d("$logCaption: timeout.")
|
|
}
|
|
|
|
else -> parseResult(requester(false))
|
|
}
|
|
|
|
// MisskeyはsinceIdを指定するとID昇順のデータが得られるので、ID降順に並べ直す
|
|
listTmp?.sortByDescending { it.getOrderId() }
|
|
|
|
fun doesAddGap() = when {
|
|
isCancelled -> false
|
|
listTmp?.isNotEmpty() != true -> false
|
|
willAddGap -> true
|
|
else -> PrefB.bpForceGap.value
|
|
}
|
|
|
|
if (doesAddGap()) {
|
|
addOne(listTmp, TootGap.mayNull(null, column.idRecent), head = addToHead)
|
|
}
|
|
|
|
return firstResult
|
|
}
|
|
|
|
private suspend fun <T : TimelineItem> refreshBottomMisskey(
|
|
logCaption: String,
|
|
requester: suspend (first: Boolean) -> TootApiResult?,
|
|
arrayFinder: (JsonObject) -> JsonArray?,
|
|
listParser: (parser: TootParser, jsonArray: JsonArray) -> List<T>,
|
|
adder: (List<T>, Boolean) -> Unit,
|
|
repeatReading: Boolean = false,
|
|
): TootApiResult? {
|
|
|
|
when (column.pagingType) {
|
|
ColumnPagingType.None ->
|
|
return TootApiResult(context.getString(R.string.end_of_list))
|
|
|
|
ColumnPagingType.Cursor ->
|
|
if (column.idOld == null) {
|
|
return TootApiResult(context.getString(R.string.end_of_list))
|
|
}
|
|
|
|
else -> Unit
|
|
}
|
|
|
|
val addToHead = false
|
|
|
|
fun parseResult(result: TootApiResult?): Boolean {
|
|
|
|
result ?: return log.d("$logCaption: cancelled.")
|
|
|
|
result.jsonObject?.let { jsonObject ->
|
|
if (column.pagingType == ColumnPagingType.Cursor) {
|
|
column.idOld = EntityId.mayNull(jsonObject.string("next"))
|
|
}
|
|
result.data = arrayFinder(jsonObject)
|
|
}
|
|
|
|
val array = result.jsonArray
|
|
?: return log.w("$logCaption: missing item list.")
|
|
|
|
val src = listParser(parser, array)
|
|
if (listTmp == null) listTmp = ArrayList(src.size)
|
|
adder(src, addToHead)
|
|
|
|
val more = when (column.pagingType) {
|
|
ColumnPagingType.Offset -> {
|
|
column.offsetNext += src.size
|
|
true
|
|
}
|
|
|
|
// ColumnPagingType.Default
|
|
else -> column.saveRangeBottom(result, src)
|
|
}
|
|
|
|
return when {
|
|
!more -> log.d("$logCaption: no more items.")
|
|
// max_id だけを指定した場合、必ずlimit個のデータが帰ってくるとは限らない
|
|
// 直前のデータが0個なら終了とみなすしかなさそう
|
|
src.isEmpty() -> log.d("$logCaption: empty item list.")
|
|
else -> true
|
|
}
|
|
}
|
|
|
|
val timeStart = SystemClock.elapsedRealtime()
|
|
val firstResult = requester(true)
|
|
var more = parseResult(firstResult) && repeatReading
|
|
while (more) more = when {
|
|
isCancelled ->
|
|
log.d("$logCaption: cancelled.")
|
|
|
|
// bottomの場合、フィルタなしなら繰り返さない
|
|
!column.isFilterEnabled ->
|
|
log.d("$logCaption: isFilterEnabled is false.")
|
|
|
|
column.idOld == null ->
|
|
log.d("$logCaption: idOld is null.")
|
|
|
|
// 十分読んだらそれで終了
|
|
(listTmp?.size ?: 0) >= Column.LOOP_READ_ENOUGH ->
|
|
log.d("$logCaption: read enough data.")
|
|
|
|
SystemClock.elapsedRealtime() - timeStart > Column.LOOP_TIMEOUT ->
|
|
log.d("$logCaption: loop timeout.")
|
|
|
|
else -> parseResult(requester(false))
|
|
}
|
|
|
|
return firstResult
|
|
}
|
|
|
|
private suspend fun <T : TimelineItem> refreshTopMastodon(
|
|
logCaption: String,
|
|
requester: suspend (first: Boolean, gapIdNewer: EntityId?, gapIdOlder: EntityId?) -> TootApiResult?,
|
|
arrayFinder: (JsonObject) -> JsonArray?,
|
|
listParser: (parser: TootParser, jsonArray: JsonArray) -> List<T>,
|
|
adder: (List<T>, Boolean) -> Unit,
|
|
): TootApiResult? {
|
|
|
|
// 上端の差分更新に対応できるのは ColumnPagingType.Default だけ
|
|
if (column.pagingType != ColumnPagingType.Default) {
|
|
return TootApiResult("can't refresh top.")
|
|
}
|
|
|
|
val addToHead = false
|
|
|
|
// 頭の方を読む時は隙間を減らすため、フィルタの有無に関係なく繰り返しを行う
|
|
var willAddGap = false
|
|
|
|
// 2回目以降のリクエスト範囲はギャップを意識したものになる
|
|
val lastSinceId = column.idRecent
|
|
var maxId: EntityId? = null
|
|
|
|
fun parseResult(result: TootApiResult?): Boolean {
|
|
val first = listTmp?.isEmpty() != false
|
|
|
|
if (result == null) {
|
|
if (!first) willAddGap = true
|
|
return log.d("$logCaption: cancelled.")
|
|
}
|
|
|
|
result.jsonObject?.let { result.data = arrayFinder(it) }
|
|
|
|
val array = result.jsonArray
|
|
if (array == null) {
|
|
if (!first) willAddGap = true
|
|
return log.w("$logCaption: missing item list")
|
|
}
|
|
|
|
val src = listParser(parser, array)
|
|
if (listTmp == null) listTmp = ArrayList(src.size)
|
|
adder(src, addToHead)
|
|
|
|
when {
|
|
first -> {
|
|
// TLは (新しいデータ)(ギャップ)(古いデータ) となるので、レンジを保存するのはここだけで良い
|
|
// 続く読み込みはギャップを埋めるものなのでレンジを保存してはいけない
|
|
column.saveRangeTop(result, src)
|
|
}
|
|
else -> {
|
|
// 今読んだのはギャップなので範囲を保存してはいけない
|
|
}
|
|
}
|
|
|
|
return when {
|
|
// max_id だけを指定した場合、必ずlimit個のデータが帰ってくるとは限らない
|
|
// 直前のデータが0個なら終了とみなすしかなさそう
|
|
src.isEmpty() -> log.d("$logCaption: empty list.")
|
|
else -> {
|
|
// 直前に読んだ範囲のmaxIdを調べる
|
|
maxId = column.parseRange(result, src).first
|
|
true
|
|
}
|
|
}
|
|
}
|
|
|
|
val timeStart = SystemClock.elapsedRealtime()
|
|
// 初回リクエスト
|
|
val firstResult = requester(true, null, null)
|
|
var more = parseResult(firstResult)
|
|
// 2回目以降
|
|
while (more) more = when {
|
|
isCancelled ->
|
|
log.d("$logCaption: cancelled.")
|
|
|
|
maxId == null ->
|
|
log.d("$logCaption: max_id is null.")
|
|
|
|
(listTmp?.size ?: 0) >= Column.LOOP_READ_ENOUGH -> {
|
|
willAddGap = true
|
|
log.d("$logCaption: read enough. make gap.")
|
|
}
|
|
|
|
SystemClock.elapsedRealtime() - timeStart > Column.LOOP_TIMEOUT -> {
|
|
willAddGap = true
|
|
log.d("$logCaption: timeout. make gap.")
|
|
}
|
|
|
|
else -> parseResult(requester(false, maxId, lastSinceId))
|
|
}
|
|
|
|
if (!isCancelled &&
|
|
listTmp?.isNotEmpty() == true &&
|
|
(willAddGap || PrefB.bpForceGap.value)
|
|
) {
|
|
addOne(listTmp, TootGap.mayNull(maxId, lastSinceId), head = addToHead)
|
|
}
|
|
return firstResult
|
|
}
|
|
|
|
private suspend fun <T : TimelineItem> refreshTopMastodonMinId(
|
|
logCaption: String,
|
|
requester: suspend (first: Boolean, gapIdNewer: EntityId?, gapIdOlder: EntityId?) -> TootApiResult?,
|
|
arrayFinder: (JsonObject) -> JsonArray?,
|
|
listParser: (parser: TootParser, jsonArray: JsonArray) -> List<T>,
|
|
adder: (List<T>, Boolean) -> Unit,
|
|
): TootApiResult? {
|
|
|
|
// 上端の差分更新に対応できるのは ColumnPagingType.Default だけ
|
|
if (column.pagingType != ColumnPagingType.Default) {
|
|
return TootApiResult("can't refresh top.")
|
|
}
|
|
|
|
val addToHead = true
|
|
|
|
var willAddGap = false
|
|
|
|
// 2回目以降のリクエスト範囲
|
|
val lastSinceId = column.idRecent
|
|
var maxId: EntityId? = null
|
|
|
|
fun parseResult(result: TootApiResult?): Boolean {
|
|
val first = listTmp?.isEmpty() != false
|
|
|
|
if (result == null) {
|
|
if (!first) willAddGap = true
|
|
return log.d("$logCaption:cancelled.")
|
|
}
|
|
|
|
result.jsonObject?.let { result.data = arrayFinder(it) }
|
|
|
|
val array = result.jsonArray
|
|
if (array == null) {
|
|
if (!first) willAddGap = true
|
|
return log.w("$logCaption: missing item list")
|
|
}
|
|
|
|
val src = listParser(parser, array)
|
|
if (listTmp == null) listTmp = ArrayList(src.size)
|
|
adder(src, addToHead)
|
|
|
|
column.saveRangeTop(result, src)
|
|
// Linkヘッダからはより新しいデータがあるかどうかはわからない
|
|
|
|
return when {
|
|
// max_id だけを指定した場合、必ずlimit個のデータが帰ってくるとは限らない
|
|
// 直前のデータが0個なら終了とみなすしかなさそう
|
|
src.isEmpty() -> log.d("$logCaption: empty list.")
|
|
|
|
else -> {
|
|
// 直前に読んだ範囲のmaxIdを調べる
|
|
maxId = column.parseRange(result, src).first
|
|
true
|
|
}
|
|
}
|
|
}
|
|
|
|
val timeStart = SystemClock.elapsedRealtime()
|
|
|
|
val firstResult = requester(true, null, null)
|
|
var more = parseResult(firstResult)
|
|
while (more) more = when {
|
|
isCancelled ->
|
|
log.d("$logCaption: cancelled.")
|
|
|
|
maxId == null ->
|
|
log.d("$logCaption: max_id is null.")
|
|
|
|
SystemClock.elapsedRealtime() - timeStart > Column.LOOP_TIMEOUT -> {
|
|
// タイムアウト
|
|
// 隙間ができるかもしれない。後ほど手動で試してもらうしかない
|
|
willAddGap = true
|
|
log.d("$logCaption: timeout. make gap.")
|
|
}
|
|
|
|
else -> parseResult(requester(false, null, null))
|
|
}
|
|
|
|
if (!isCancelled &&
|
|
listTmp?.isNotEmpty() == true &&
|
|
(willAddGap || PrefB.bpForceGap.value)
|
|
) {
|
|
addOne(listTmp, TootGap.mayNull(maxId, lastSinceId), head = addToHead)
|
|
}
|
|
|
|
return firstResult
|
|
}
|
|
|
|
private suspend fun <T : TimelineItem> refreshBottomMastodon(
|
|
logCaption: String,
|
|
requester: suspend (first: Boolean, gapIdNewer: EntityId?, gapIdOlder: EntityId?) -> TootApiResult?,
|
|
arrayFinder: (JsonObject) -> JsonArray?,
|
|
listParser: (parser: TootParser, jsonArray: JsonArray) -> List<T>,
|
|
adder: (List<T>, Boolean) -> Unit,
|
|
repeatReading: Boolean = false,
|
|
): TootApiResult? {
|
|
|
|
when (column.pagingType) {
|
|
ColumnPagingType.None ->
|
|
return TootApiResult(context.getString(R.string.end_of_list))
|
|
|
|
ColumnPagingType.Cursor ->
|
|
if (column.idOld == null) {
|
|
return TootApiResult(context.getString(R.string.end_of_list))
|
|
}
|
|
|
|
else -> Unit
|
|
}
|
|
|
|
val addToHead = false
|
|
|
|
// parse result and add to list_tmp
|
|
// returns false if no more result
|
|
fun parseResult(result: TootApiResult?): Boolean {
|
|
result ?: return log.d("$logCaption: cancelled.")
|
|
|
|
result.jsonObject?.let { it ->
|
|
if (column.pagingType == ColumnPagingType.Cursor) {
|
|
column.idOld = EntityId.mayNull(it.string("next"))
|
|
}
|
|
result.data = arrayFinder(it)
|
|
}
|
|
val array = result.jsonArray
|
|
?: return log.d("$logCaption: missing item list.")
|
|
|
|
val src = listParser(parser, array)
|
|
if (listTmp == null) listTmp = ArrayList(src.size)
|
|
adder(src, addToHead)
|
|
|
|
// save range to column
|
|
// false if no more result
|
|
val more = when (column.pagingType) {
|
|
ColumnPagingType.Offset -> {
|
|
column.offsetNext += src.size
|
|
true
|
|
}
|
|
|
|
// ColumnPagingType.Default
|
|
else -> column.saveRangeBottom(result, src)
|
|
}
|
|
|
|
return when {
|
|
!more -> log.d("$logCaption: no more items.")
|
|
|
|
// max_id だけを指定した場合、必ずlimit個のデータが帰ってくるとは限らない
|
|
// 直前のデータが0個なら終了とみなすしかなさそう
|
|
src.isEmpty() -> log.d("$logCaption: empty item list.")
|
|
|
|
else -> true
|
|
}
|
|
}
|
|
|
|
val timeStart = SystemClock.elapsedRealtime()
|
|
val firstResult = requester(true, null, null)
|
|
var more = parseResult(firstResult) && repeatReading
|
|
while (more) more = when {
|
|
isCancelled ->
|
|
log.d("$logCaption: cancelled.")
|
|
|
|
// bottomの場合、フィルタなしなら繰り返さない
|
|
!column.isFilterEnabled ->
|
|
log.d("$logCaption: isFiltered is false.")
|
|
|
|
column.idOld == null ->
|
|
log.d("$logCaption: idOld is null.")
|
|
|
|
(listTmp?.size ?: 0) >= Column.LOOP_READ_ENOUGH ->
|
|
log.d("$logCaption: read enough data.")
|
|
|
|
SystemClock.elapsedRealtime() - timeStart > Column.LOOP_TIMEOUT ->
|
|
log.d("$logCaption: loop timeout.")
|
|
|
|
else -> parseResult(requester(false, null, null))
|
|
}
|
|
|
|
return firstResult
|
|
}
|
|
|
|
private suspend fun defaultRequesterMisskey(
|
|
client: TootApiClient,
|
|
pathBase: String,
|
|
params: JsonObject,
|
|
first: Boolean,
|
|
) = client.request(
|
|
pathBase,
|
|
params.apply {
|
|
if (!bBottom) {
|
|
if (first) {
|
|
addRangeMisskey(bBottom = false)
|
|
} else {
|
|
putMisskeySince(column.idRecent)
|
|
}
|
|
} else {
|
|
if (first) {
|
|
when (column.pagingType) {
|
|
ColumnPagingType.Default -> addRangeMisskey(bBottom = true)
|
|
ColumnPagingType.Offset -> put("offset", column.offsetNext)
|
|
ColumnPagingType.Cursor -> put("cursor", column.idOld)
|
|
|
|
ColumnPagingType.None -> {
|
|
}
|
|
}
|
|
} else {
|
|
when (column.pagingType) {
|
|
ColumnPagingType.Default -> putMisskeyUntil(column.idOld)
|
|
ColumnPagingType.Offset -> put("offset", column.offsetNext)
|
|
ColumnPagingType.Cursor -> put("cursor", column.idOld)
|
|
|
|
ColumnPagingType.None -> {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}.toPostRequestBuilder()
|
|
)
|
|
|
|
private suspend fun defaultRequesterMastodon(
|
|
client: TootApiClient,
|
|
pathBase: String,
|
|
delimiter: Char,
|
|
@Suppress("UNUSED_PARAMETER") first: Boolean,
|
|
useMinId: Boolean,
|
|
gapIdNewer: EntityId?,
|
|
gapIdOlder: EntityId?,
|
|
) =
|
|
client.request(
|
|
when {
|
|
// profile directory 用
|
|
column.pagingType == ColumnPagingType.Offset ->
|
|
"$pathBase${delimiter}offset=${column.offsetNext}"
|
|
|
|
useMinId ->
|
|
column.addRangeMin(pathBase)
|
|
|
|
gapIdNewer != null ->
|
|
"$pathBase${delimiter}max_id=$gapIdNewer&since_id=$gapIdOlder"
|
|
|
|
else ->
|
|
column.addRange(bBottom = bBottom, pathBase, delimiter = delimiter)
|
|
}
|
|
)
|
|
|
|
suspend fun getStatusList(
|
|
client: TootApiClient,
|
|
pathBase: String?,
|
|
useMinId: Boolean = false,
|
|
misskeyParams: JsonObject? = null,
|
|
listParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootStatus> =
|
|
defaultStatusListParser,
|
|
): TootApiResult? {
|
|
|
|
pathBase ?: return null // cancelled.
|
|
|
|
val logCaption = "getStatusList"
|
|
val adder: (List<TootStatus>, Boolean) -> Unit =
|
|
{ src, head -> addWithFilterStatus(listTmp, src, head = head) }
|
|
|
|
return if (isMisskey) {
|
|
|
|
val params = misskeyParams ?: column.makeMisskeyTimelineParameter(parser)
|
|
|
|
val requester: suspend (Boolean) -> TootApiResult? =
|
|
{ defaultRequesterMisskey(client, pathBase, params, it) }
|
|
|
|
when {
|
|
bBottom -> refreshBottomMisskey(
|
|
logCaption,
|
|
requester = requester,
|
|
arrayFinder = nullArrayFinder,
|
|
listParser = listParser,
|
|
adder = adder,
|
|
repeatReading = true
|
|
)
|
|
else -> refreshTopMisskey(
|
|
logCaption,
|
|
requester = requester,
|
|
arrayFinder = nullArrayFinder,
|
|
listParser = listParser,
|
|
adder = adder
|
|
)
|
|
}
|
|
} else {
|
|
|
|
val delimiter = if (-1 != pathBase.indexOf('?')) '&' else '?'
|
|
|
|
val requester: suspend (first: Boolean, gapIdNewer: EntityId?, gapIdOlder: EntityId?) -> TootApiResult? =
|
|
{ first, gapIdNewer, gapIdOlder ->
|
|
defaultRequesterMastodon(
|
|
client,
|
|
pathBase,
|
|
delimiter,
|
|
first,
|
|
useMinId = useMinId,
|
|
gapIdNewer,
|
|
gapIdOlder,
|
|
)
|
|
}
|
|
|
|
when {
|
|
bBottom -> refreshBottomMastodon(
|
|
logCaption,
|
|
requester = requester,
|
|
arrayFinder = nullArrayFinder,
|
|
listParser = listParser,
|
|
adder = adder,
|
|
repeatReading = true
|
|
)
|
|
useMinId -> refreshTopMastodonMinId(
|
|
logCaption,
|
|
requester = requester,
|
|
arrayFinder = nullArrayFinder,
|
|
listParser = listParser,
|
|
adder
|
|
)
|
|
else -> refreshTopMastodon(
|
|
logCaption,
|
|
requester = requester,
|
|
arrayFinder = nullArrayFinder,
|
|
listParser = listParser,
|
|
adder
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
suspend fun getNotificationList(
|
|
client: TootApiClient,
|
|
fromAcct: String? = null,
|
|
useMinId: Boolean = false,
|
|
): TootApiResult? {
|
|
|
|
val logCaption = "getNotificationList"
|
|
|
|
val listParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootNotification> =
|
|
defaultNotificationListParser
|
|
|
|
val adder: (List<TootNotification>, Boolean) -> Unit =
|
|
{ src, head -> addWithFilterNotification(listTmp, src, head = head) }
|
|
|
|
// Misskeyの通知TLはfromAcctに対応していない
|
|
val pathBase = column.makeNotificationUrl(client, fromAcct)
|
|
|
|
return if (isMisskey) {
|
|
|
|
val params =
|
|
column.makeMisskeyBaseParameter(parser).addMisskeyNotificationFilter(column)
|
|
|
|
val requester: suspend (Boolean) -> TootApiResult? =
|
|
{ defaultRequesterMisskey(client, pathBase, params, it) }
|
|
|
|
when {
|
|
bBottom -> refreshBottomMisskey(
|
|
logCaption,
|
|
requester = requester,
|
|
arrayFinder = nullArrayFinder,
|
|
listParser = listParser,
|
|
adder = adder,
|
|
repeatReading = true
|
|
)
|
|
else -> refreshTopMisskey(
|
|
logCaption,
|
|
requester = requester,
|
|
arrayFinder = nullArrayFinder,
|
|
listParser = listParser,
|
|
adder = adder
|
|
)
|
|
}
|
|
} else {
|
|
|
|
val delimiter = if (-1 != pathBase.indexOf('?')) '&' else '?'
|
|
|
|
val requester: suspend (first: Boolean, gapIdNewer: EntityId?, gapIdOlder: EntityId?) -> TootApiResult? =
|
|
{ first, gapIdNewer, gapIdOlder ->
|
|
defaultRequesterMastodon(
|
|
client,
|
|
pathBase,
|
|
delimiter,
|
|
first,
|
|
useMinId = useMinId,
|
|
gapIdNewer,
|
|
gapIdOlder,
|
|
)
|
|
}
|
|
|
|
when {
|
|
bBottom -> refreshBottomMastodon(
|
|
logCaption,
|
|
requester = requester,
|
|
arrayFinder = nullArrayFinder,
|
|
listParser = listParser,
|
|
adder = adder,
|
|
repeatReading = true
|
|
)
|
|
useMinId -> refreshTopMastodonMinId(
|
|
logCaption,
|
|
requester = requester,
|
|
arrayFinder = nullArrayFinder,
|
|
listParser = listParser,
|
|
adder = adder
|
|
)
|
|
else -> refreshTopMastodon(
|
|
logCaption,
|
|
requester = requester,
|
|
arrayFinder = nullArrayFinder,
|
|
listParser = listParser,
|
|
adder = adder
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
suspend fun getAccountList(
|
|
client: TootApiClient,
|
|
pathBase: String,
|
|
misskeyParams: JsonObject? = null,
|
|
arrayFinder: (JsonObject) -> JsonArray? =
|
|
nullArrayFinder,
|
|
listParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootAccountRef> =
|
|
defaultAccountListParser,
|
|
): TootApiResult? {
|
|
|
|
val logCaption = "getAccountList"
|
|
val adder: (List<TootAccountRef>, Boolean) -> Unit =
|
|
{ src, head -> addAll(listTmp, src, head = head) }
|
|
|
|
return if (isMisskey) {
|
|
val params = misskeyParams ?: column.makeMisskeyBaseParameter(parser)
|
|
val requester: suspend (Boolean) -> TootApiResult? =
|
|
{ defaultRequesterMisskey(client, pathBase, params, it) }
|
|
when {
|
|
bBottom -> refreshBottomMisskey(
|
|
logCaption,
|
|
requester = requester,
|
|
arrayFinder = arrayFinder,
|
|
listParser = listParser,
|
|
adder = adder
|
|
)
|
|
else -> refreshTopMisskey(
|
|
logCaption,
|
|
requester = requester,
|
|
arrayFinder = arrayFinder,
|
|
listParser = listParser,
|
|
adder = adder
|
|
)
|
|
}
|
|
} else {
|
|
val delimiter = if (-1 != pathBase.indexOf('?')) '&' else '?'
|
|
val requester: suspend (first: Boolean, gapIdNewer: EntityId?, gapIdOlder: EntityId?) -> TootApiResult? =
|
|
{ first, gapIdNewer, gapIdOlder ->
|
|
defaultRequesterMastodon(
|
|
client,
|
|
pathBase,
|
|
delimiter,
|
|
first,
|
|
useMinId = false,
|
|
gapIdNewer,
|
|
gapIdOlder,
|
|
)
|
|
}
|
|
|
|
when {
|
|
bBottom -> refreshBottomMastodon(
|
|
logCaption,
|
|
requester = requester,
|
|
arrayFinder = arrayFinder,
|
|
listParser = listParser,
|
|
adder = adder
|
|
)
|
|
else -> refreshTopMastodon(
|
|
logCaption,
|
|
requester = requester,
|
|
arrayFinder = arrayFinder,
|
|
listParser = listParser,
|
|
adder = adder
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
suspend fun getDomainList(
|
|
client: TootApiClient,
|
|
pathBase: String,
|
|
): TootApiResult? {
|
|
|
|
val logCaption = "getDomainList"
|
|
|
|
val adder: (List<TimelineItem>, Boolean) -> Unit =
|
|
{ src, head -> addAll(listTmp, src, head = head) }
|
|
|
|
val listParser = defaultDomainBlockListParser
|
|
|
|
return if (isMisskey) {
|
|
TootApiResult("misskey support is not yet implemented.")
|
|
} else {
|
|
// ページングIDはサーバ側の内部IDで、Linkヘッダ以外には露出しない。
|
|
val delimiter = if (-1 != pathBase.indexOf('?')) '&' else '?'
|
|
val requester: suspend (first: Boolean, gapIdNewer: EntityId?, gapIdOlder: EntityId?) -> TootApiResult? =
|
|
{ first, gapIdNewer, gapIdOlder ->
|
|
defaultRequesterMastodon(
|
|
client,
|
|
pathBase,
|
|
delimiter,
|
|
first,
|
|
useMinId = false,
|
|
gapIdNewer,
|
|
gapIdOlder,
|
|
)
|
|
}
|
|
|
|
when {
|
|
bBottom -> refreshBottomMastodon(
|
|
logCaption,
|
|
requester = requester,
|
|
arrayFinder = nullArrayFinder,
|
|
listParser = listParser,
|
|
adder = adder
|
|
)
|
|
else -> refreshTopMastodon(
|
|
logCaption,
|
|
requester = requester,
|
|
arrayFinder = nullArrayFinder,
|
|
listParser = listParser,
|
|
adder = adder
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// リスト一覧にはページネーションがない
|
|
// fun getListList(client : TootApiClient, path_base : String) : TootApiResult? {
|
|
//
|
|
// if(isMisskey) return TootApiResult("misskey support is not yet implemented.")
|
|
//
|
|
// return TootApiResult("Mastodon's /api/v1/lists has no pagination.")
|
|
// }
|
|
|
|
suspend fun getReportList(
|
|
client: TootApiClient,
|
|
pathBase: String,
|
|
): TootApiResult? {
|
|
|
|
val logCaption = "getReportList"
|
|
val adder: (List<TootReport>, Boolean) -> Unit =
|
|
{ src, head -> addAll(listTmp, src, head = head) }
|
|
|
|
val listParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootReport> =
|
|
{ _, jsonArray -> parseList(::TootReport, jsonArray) }
|
|
|
|
return if (isMisskey) {
|
|
TootApiResult("Misskey has no API to list reports from you.")
|
|
} else {
|
|
// ページングIDはサーバ側の内部IDで、Linkヘッダ以外には露出しない。
|
|
val delimiter = if (-1 != pathBase.indexOf('?')) '&' else '?'
|
|
val requester: suspend (first: Boolean, gapIdNewer: EntityId?, gapIdOlder: EntityId?) -> TootApiResult? =
|
|
{ first, gapIdNewer, gapIdOlder ->
|
|
defaultRequesterMastodon(
|
|
client,
|
|
pathBase,
|
|
delimiter,
|
|
first,
|
|
useMinId = false,
|
|
gapIdNewer,
|
|
gapIdOlder,
|
|
)
|
|
}
|
|
|
|
when {
|
|
bBottom -> refreshBottomMastodon(
|
|
logCaption,
|
|
requester = requester,
|
|
arrayFinder = nullArrayFinder,
|
|
listParser = listParser,
|
|
adder = adder
|
|
)
|
|
else -> refreshTopMastodon(
|
|
logCaption,
|
|
requester = requester,
|
|
arrayFinder = nullArrayFinder,
|
|
listParser = listParser,
|
|
adder = adder
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
suspend fun getConversationSummaryList(
|
|
client: TootApiClient,
|
|
pathBase: String,
|
|
// aroundMin : Boolean = false,
|
|
misskeyParams: JsonObject? = null,
|
|
): TootApiResult? {
|
|
val logCaption = "getConversationSummaryList"
|
|
|
|
val listParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootConversationSummary> =
|
|
defaultConversationSummaryListParser
|
|
|
|
val adder: (List<TootConversationSummary>, Boolean) -> Unit =
|
|
{ src, head -> addWithFilterConversationSummary(listTmp, src, head = head) }
|
|
|
|
return if (isMisskey) {
|
|
|
|
val params = misskeyParams ?: column.makeMisskeyTimelineParameter(parser)
|
|
|
|
val requester: suspend (Boolean) -> TootApiResult? =
|
|
{ defaultRequesterMisskey(client, pathBase, params, it) }
|
|
|
|
when {
|
|
bBottom -> refreshBottomMisskey(
|
|
logCaption,
|
|
requester = requester,
|
|
arrayFinder = nullArrayFinder,
|
|
listParser = listParser,
|
|
adder = adder,
|
|
repeatReading = true
|
|
)
|
|
else -> refreshTopMisskey(
|
|
logCaption,
|
|
requester = requester,
|
|
arrayFinder = nullArrayFinder,
|
|
listParser = listParser,
|
|
adder = adder
|
|
)
|
|
}
|
|
} else {
|
|
val delimiter = if (-1 != pathBase.indexOf('?')) '&' else '?'
|
|
val requester: suspend (first: Boolean, gapIdNewer: EntityId?, gapIdOlder: EntityId?) -> TootApiResult? =
|
|
{ first, gapIdNewer, gapIdOlder ->
|
|
defaultRequesterMastodon(
|
|
client,
|
|
pathBase,
|
|
delimiter,
|
|
first,
|
|
useMinId = false,
|
|
gapIdNewer,
|
|
gapIdOlder,
|
|
)
|
|
}
|
|
|
|
when {
|
|
bBottom -> refreshBottomMastodon(
|
|
logCaption,
|
|
requester = requester,
|
|
arrayFinder = nullArrayFinder,
|
|
listParser = listParser,
|
|
adder = adder,
|
|
repeatReading = true
|
|
)
|
|
else -> refreshTopMastodon(
|
|
logCaption,
|
|
requester = requester,
|
|
arrayFinder = nullArrayFinder,
|
|
listParser = listParser,
|
|
adder
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
suspend fun getScheduledStatuses(client: TootApiClient): TootApiResult? {
|
|
val result = client.request(column.addRange(bBottom, ApiPath.PATH_SCHEDULED_STATUSES))
|
|
val src = parseList(::TootScheduled, parser, result?.jsonArray)
|
|
listTmp = addAll(listTmp, src)
|
|
column.saveRange(bBottom, !bBottom, result, src)
|
|
return result
|
|
}
|
|
|
|
suspend fun getEditHistory(client: TootApiClient): TootApiResult? {
|
|
// ページングなし
|
|
val result = client.request("/api/v1/statuses/${column.statusId}/history")
|
|
|
|
// TootStatusとしては不足している情報があるのを補う
|
|
TootStatus.supplyEditHistory(result?.jsonArray, column.originalStatus)
|
|
|
|
val src = parser.statusList(result?.jsonArray).reversed()
|
|
listTmp = addAll(listTmp, src)
|
|
column.saveRange(bBottom, !bBottom, result, src)
|
|
return result
|
|
}
|
|
|
|
suspend fun getFollowedHashtags(client: TootApiClient): TootApiResult? {
|
|
val path = column.addRange(bBottom = bBottom, "/api/v1/followed_tags")
|
|
val result = client.request(path)
|
|
val src = parser.tagList(result?.jsonArray)
|
|
listTmp = addAll(listTmp, src)
|
|
column.saveRange(bBottom = bBottom, bTop = !bBottom, result = result, list = src)
|
|
return result
|
|
}
|
|
}
|