SubwayTooter-Android-App/app/src/main/java/jp/juggler/subwaytooter/column/ColumnActions.kt

469 lines
15 KiB
Kotlin

package jp.juggler.subwaytooter.column
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.columnviewholder.onListListUpdated
import jp.juggler.subwaytooter.notification.onNotificationCleared
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.BucketList
import jp.juggler.subwaytooter.util.matchHost
import jp.juggler.util.data.JsonObject
import jp.juggler.util.log.LogCategory
import jp.juggler.util.ui.AdapterChange
import jp.juggler.util.ui.AdapterChangeType
import kotlin.collections.set
private val log = LogCategory("ColumnActions")
/*
なんらかアクションを行った後にカラムデータを更新する処理など
*/
// 予約した投稿を削除した後の処理
fun Column.onScheduleDeleted(item: TootScheduled) {
val tmpList = ArrayList<TimelineItem>(listData.size)
for (o in listData) {
if (o === item) continue
tmpList.add(o)
}
if (tmpList.size != listData.size) {
listData.clear()
listData.addAll(tmpList)
fireShowContent(reason = "onScheduleDeleted")
}
}
// ステータスが削除された時に呼ばれる
fun Column.onStatusRemoved(tlHost: Host, statusId: EntityId) {
if (isDispose.get() || bInitialLoading || bRefreshLoading) return
if (!accessInfo.matchHost(tlHost)) return
val tmpList = ArrayList<TimelineItem>(listData.size)
for (o in listData) {
if (o is TootStatus) {
if (statusId == o.id) continue
if (statusId == (o.reblog?.id ?: -1L)) continue
} else if (o is TootNotification) {
val s = o.status
if (s != null) {
if (statusId == s.id) continue
if (statusId == (s.reblog?.id ?: -1L)) continue
}
}
tmpList.add(o)
}
if (tmpList.size != listData.size) {
listData.clear()
listData.addAll(tmpList)
fireShowContent(reason = "removeStatus")
}
}
// ブーストやお気に入りの更新に使う。ステータスを列挙する。
fun Column.findStatus(
targetApDomain: Host,
targetStatusId: EntityId,
callback: (account: SavedAccount, status: TootStatus) -> Boolean,
// callback return true if rebind view required
) {
if (!accessInfo.matchHost(targetApDomain)) return
var bChanged = false
fun procStatus(status: TootStatus?) {
if (status != null) {
if (targetStatusId == status.id) {
if (callback(accessInfo, status)) bChanged = true
}
procStatus(status.reblog)
}
}
for (data in listData) {
when (data) {
is TootNotification -> procStatus(data.status)
is TootStatus -> procStatus(data)
}
}
if (bChanged) fireRebindAdapterItems()
}
private const val INVALID_ACCOUNT = -1L
// ミュート、ブロックが成功した時に呼ばれる
// リストメンバーカラムでメンバーをリストから除去した時に呼ばれる
fun Column.removeAccountInTimeline(
targetAccount: SavedAccount,
whoId: EntityId,
removeFromUserList: Boolean = false,
) {
if (targetAccount != accessInfo) return
val tmpList = ArrayList<TimelineItem>(listData.size)
for (o in listData) {
if (o is TootStatus) {
if (whoId == (o.account.id)) continue
if (whoId == (o.reblog?.account?.id ?: INVALID_ACCOUNT)) continue
} else if (o is TootNotification) {
if (whoId == (o.account?.id ?: INVALID_ACCOUNT)) continue
if (whoId == (o.status?.account?.id ?: INVALID_ACCOUNT)) continue
if (whoId == (o.status?.reblog?.account?.id ?: INVALID_ACCOUNT)) continue
} else if (o is TootAccountRef && removeFromUserList) {
if (whoId == o.get().id) continue
}
tmpList.add(o)
}
if (tmpList.size != listData.size) {
listData.clear()
listData.addAll(tmpList)
fireShowContent(reason = "removeAccountInTimeline")
}
}
// ミュート、ブロックが成功した時に呼ばれる
// リストメンバーカラムでメンバーをリストから除去した時に呼ばれる
// require full acct
fun Column.removeAccountInTimelinePseudo(acct: Acct) {
val tmpList = ArrayList<TimelineItem>(listData.size)
for (o in listData) {
if (o is TootStatus) {
if (acct == accessInfo.getFullAcct(o.account)) continue
if (acct == accessInfo.getFullAcct(o.reblog?.account)) continue
} else if (o is TootNotification) {
if (acct == accessInfo.getFullAcct(o.account)) continue
if (acct == accessInfo.getFullAcct(o.status?.account)) continue
if (acct == accessInfo.getFullAcct(o.status?.reblog?.account)) continue
}
tmpList.add(o)
}
if (tmpList.size != listData.size) {
listData.clear()
listData.addAll(tmpList)
fireShowContent(reason = "removeAccountInTimelinePseudo")
}
}
// misskeyカラムやプロフカラムでブロック成功した時に呼ばれる
fun Column.updateFollowIcons(targetAccount: SavedAccount) {
if (targetAccount != accessInfo) return
fireShowContent(reason = "updateFollowIcons", reset = true)
}
// ユーザのブロック、ミュート、フォロー推奨の削除、フォローリクエストの承認/却下などから呼ばれる
fun Column.removeUser(targetAccount: SavedAccount, columnType: ColumnType, whoId: EntityId) {
if (type == columnType && targetAccount == accessInfo) {
val tmpList = ArrayList<TimelineItem>(listData.size)
for (o in listData) {
if (o is TootAccountRef) {
if (o.get().id == whoId) continue
}
tmpList.add(o)
}
if (tmpList.size != listData.size) {
listData.clear()
listData.addAll(tmpList)
fireShowContent(reason = "removeUser")
}
}
}
// 通知カラムの通知を全て削除した後に呼ばれる
fun Column.removeNotifications() {
cancelLastTask()
mRefreshLoadingErrorPopupState = 0
mRefreshLoadingError = ""
bRefreshLoading = false
mInitialLoadingError = ""
bInitialLoading = false
idOld = null
idRecent = null
offsetNext = 0
pagingType = ColumnPagingType.Default
listData.clear()
duplicateMap.clear()
fireShowContent(reason = "removeNotifications", reset = true)
onNotificationCleared(accessInfo.db_id)
}
// 通知を削除した後に呼ばれる
fun Column.removeNotificationOne(targetAccount: SavedAccount, notification: TootNotification) {
if (!isNotificationColumn) return
if (accessInfo != targetAccount) return
val tmpList = ArrayList<TimelineItem>(listData.size)
for (o in listData) {
if (o is TootNotification) {
if (o.id == notification.id) continue
}
tmpList.add(o)
}
if (tmpList.size != listData.size) {
listData.clear()
listData.addAll(tmpList)
fireShowContent(reason = "removeNotificationOne")
}
}
fun Column.onMuteUpdated() {
val checker = { status: TootStatus? -> status?.checkMuted() ?: false }
val tmpList = ArrayList<TimelineItem>(listData.size)
for (o in listData) {
if (o is TootStatus) {
if (checker(o)) continue
}
if (o is TootNotification) {
if (checker(o.status)) continue
}
tmpList.add(o)
}
if (tmpList.size != listData.size) {
listData.clear()
listData.addAll(tmpList)
fireShowContent(reason = "onMuteUpdated")
}
}
fun Column.replaceStatus(statusId: EntityId, statusJson: JsonObject) {
if (type == ColumnType.STATUS_HISTORY) return
fun createStatus() =
TootParser(context, accessInfo).status(statusJson)
?: error("replaceStatus: parse failed.")
val tmpList = ArrayList(listData)
var changed = false
for (i in 0 until tmpList.size) {
when (val item = tmpList[i]) {
is TootStatus -> {
if (item.id == statusId) {
tmpList[i] = createStatus()
changed = true
} else if (item.reblog?.id == statusId) {
item.reblog = createStatus().also { it.reblogParent = item }
changed = true
}
}
is TootNotification -> {
if (item.status?.id == statusId) {
item.status = createStatus()
changed = true
}
}
}
}
if (changed) {
listData.clear()
listData.addAll(tmpList)
fireShowContent(reason = "replaceStatus")
}
}
fun Column.onHideFavouriteNotification(acct: Acct) {
if (!isNotificationColumn) return
val tmpList = ArrayList<TimelineItem>(listData.size)
for (o in listData) {
if (o is TootNotification && o.type != TootNotification.TYPE_MENTION) {
val who = o.account
if (who != null) {
val whoAcct = accessInfo.getFullAcct(who)
if (whoAcct == acct) continue
}
}
tmpList.add(o)
}
if (tmpList.size != listData.size) {
listData.clear()
listData.addAll(tmpList)
fireShowContent(reason = "onHideFavouriteNotification")
}
}
fun Column.onDomainBlockChanged(
targetAccount: SavedAccount,
domain: Host,
bBlocked: Boolean,
) {
if (targetAccount.apiHost != accessInfo.apiHost) return
if (accessInfo.isPseudo) return
if (type == ColumnType.DOMAIN_BLOCKS) {
// ドメインブロック一覧を読み直す
startLoading()
return
}
if (bBlocked) {
// ブロックしたのとドメイン部分が一致するアカウントからのステータスと通知をすべて除去する
val checker =
{ account: TootAccount? -> if (account == null) false else account.acct.host == domain }
val tmpList = ArrayList<TimelineItem>(listData.size)
for (o in listData) {
if (o is TootStatus) {
if (checker(o.account)) continue
if (checker(o.reblog?.account)) continue
} else if (o is TootNotification) {
if (checker(o.account)) continue
if (checker(o.status?.account)) continue
if (checker(o.status?.reblog?.account)) continue
}
tmpList.add(o)
}
if (tmpList.size != listData.size) {
listData.clear()
listData.addAll(tmpList)
fireShowContent(reason = "onDomainBlockChanged")
}
}
}
fun Column.onListListUpdated(account: SavedAccount) {
if (account != accessInfo) return
if (type == ColumnType.LIST_LIST || type == ColumnType.MISSKEY_ANTENNA_LIST) {
startLoading()
val vh = viewHolder
vh?.onListListUpdated()
}
}
fun Column.onListNameUpdated(account: SavedAccount, item: TootList) {
if (account != accessInfo) return
if (type == ColumnType.LIST_LIST) {
startLoading()
} else if (type == ColumnType.LIST_TL || type == ColumnType.LIST_MEMBER) {
if (item.id == profileId) {
this.listInfo = item
fireShowColumnHeader()
}
}
}
// fun onAntennaNameUpdated(account : SavedAccount, item : MisskeyAntenna) {
// if(account != access_info) return
// if(type == ColumnType.MISSKEY_ANTENNA_LIST) {
// startLoading()
// } else if(type == ColumnType.MISSKEY_ANTENNA_TL) {
// if(item.id == profile_id) {
// this.antenna_info = item
// fireShowColumnHeader()
// }
// }
// }
fun Column.onListMemberUpdated(
account: SavedAccount,
listId: EntityId,
who: TootAccount,
bAdd: Boolean,
) {
if (type == ColumnType.LIST_TL && accessInfo == account && listId == profileId) {
if (!bAdd) {
removeAccountInTimeline(account, who.id)
}
} else if (type == ColumnType.LIST_MEMBER && accessInfo == account && listId == profileId) {
if (!bAdd) {
removeAccountInTimeline(account, who.id)
}
}
}
// 既存データ中の会話サマリ項目と追加データの中にIDが同じものがあれば
// 既存データを入れ替えて追加データから削除するか
// 既存データを削除するかする
fun replaceConversationSummary(
changeList: ArrayList<AdapterChange>,
listNew: ArrayList<TimelineItem>,
listData: BucketList<TimelineItem>,
) {
val newMap = HashMap<EntityId, TootConversationSummary>().apply {
for (o in listNew) {
if (o is TootConversationSummary) this[o.id] = o
}
}
if (listData.isEmpty() || newMap.isEmpty()) return
val removeSet = HashSet<EntityId>()
for (i in listData.size - 1 downTo 0) {
val o = listData[i] as? TootConversationSummary ?: continue
val newItem = newMap[o.id] ?: continue
if (o.last_status.uri == newItem.last_status.uri) {
// 投稿が同じなので順序を入れ替えず、その場所で更新する
changeList.add(AdapterChange(AdapterChangeType.RangeChange, i, 1))
listData[i] = newItem
removeSet.add(newItem.id)
log.d("replaceConversationSummary: in-place update")
} else {
// 投稿が異なるので古い方を削除して、リストの順序を変える
changeList.add(AdapterChange(AdapterChangeType.RangeRemove, i, 1))
listData.removeAt(i)
log.d("replaceConversationSummary: order change")
}
}
val it = listNew.iterator()
while (it.hasNext()) {
val o = it.next() as? TootConversationSummary ?: continue
if (removeSet.contains(o.id)) it.remove()
}
}
// タグのフォロー状態が変わったら呼ばれる
fun Column.onTagFollowChanged(account: SavedAccount, newTag: TootTag) {
if (isDispose.get() || bInitialLoading || bRefreshLoading) return
if (accessInfo != account) return
when (type) {
ColumnType.FOLLOWED_HASHTAGS, ColumnType.TREND_TAG, ColumnType.SEARCH -> {
val tmpList = ArrayList<TimelineItem>(listData.size)
for (o in listData) {
if (o is TootTag && o.name == newTag.name) {
tmpList.add(newTag)
} else {
tmpList.add(o)
}
}
if (type == ColumnType.FOLLOWED_HASHTAGS) {
val tagFinder: (TimelineItem) -> Boolean =
{ it is TootTag && it.name == newTag.name }
when (newTag.following) {
true ->
if (tmpList.none(tagFinder)) {
tmpList.add(0, newTag)
}
else -> tmpList.indexOfFirst(tagFinder)
.takeIf { it >= 0 }?.let { tmpList.removeAt(it) }
}
}
listData.clear()
listData.addAll(tmpList)
fireShowContent(reason = "onTagFollowChanged")
}
else -> Unit
}
}