refactor column class
This commit is contained in:
parent
a2d91673e6
commit
d831215ac4
|
@ -244,7 +244,7 @@ class ActColumnCustomize : AppCompatActivity(), View.OnClickListener, ColorPicke
|
|||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
override suspend fun background(client: TootApiClient): TootApiResult {
|
||||
try {
|
||||
val backgroundDir = Column.getBackgroundImageDir(this@ActColumnCustomize)
|
||||
val backgroundDir = getBackgroundImageDir(this@ActColumnCustomize)
|
||||
val file =
|
||||
File(backgroundDir, "${column.column_id}:${System.currentTimeMillis()}")
|
||||
val fileUri = Uri.fromFile(file)
|
||||
|
|
|
@ -119,7 +119,7 @@ class ActColumnList : AppCompatActivity() {
|
|||
// 左にスワイプした(右端に青が見えた) なら要素を削除する
|
||||
if(swipedDirection == ListSwipeItem.SwipeDirection.LEFT) {
|
||||
val adapterItem = item.tag as MyItem
|
||||
if(adapterItem.json.optBoolean(Column.KEY_DONT_CLOSE, false)) {
|
||||
if(adapterItem.json.optBoolean(ColumnEncoder.KEY_DONT_CLOSE, false)) {
|
||||
showToast(false, R.string.column_has_dont_close_option)
|
||||
listView.resetSwipedViews(null)
|
||||
return
|
||||
|
@ -193,13 +193,13 @@ class ActColumnList : AppCompatActivity() {
|
|||
// リスト要素のデータ
|
||||
internal class MyItem(val json : JsonObject, val id : Long, context : Context) {
|
||||
|
||||
val name : String = json.optString(Column.KEY_COLUMN_NAME)
|
||||
val acct : Acct = Acct.parse(json.optString(Column.KEY_COLUMN_ACCESS_ACCT))
|
||||
val acct_name : String = json.optString(Column.KEY_COLUMN_ACCESS_STR)
|
||||
val old_index = json.optInt(Column.KEY_OLD_INDEX)
|
||||
val type = ColumnType.parse(json.optInt(Column.KEY_TYPE))
|
||||
val acct_color_bg = json.optInt(Column.KEY_COLUMN_ACCESS_COLOR_BG, 0)
|
||||
val acct_color_fg = json.optInt(Column.KEY_COLUMN_ACCESS_COLOR, 0)
|
||||
val name : String = json.optString(ColumnEncoder.KEY_COLUMN_NAME)
|
||||
val acct : Acct = Acct.parse(json.optString(ColumnEncoder.KEY_COLUMN_ACCESS_ACCT))
|
||||
val acct_name : String = json.optString(ColumnEncoder.KEY_COLUMN_ACCESS_STR)
|
||||
val old_index = json.optInt(ColumnEncoder.KEY_OLD_INDEX)
|
||||
val type = ColumnType.parse(json.optInt(ColumnEncoder.KEY_TYPE))
|
||||
val acct_color_bg = json.optInt(ColumnEncoder.KEY_COLUMN_ACCESS_COLOR_BG, 0)
|
||||
val acct_color_fg = json.optInt(ColumnEncoder.KEY_COLUMN_ACCESS_COLOR, 0)
|
||||
.notZero() ?: context.attrColor(R.attr.colorColumnListItemText)
|
||||
var bOldSelection : Boolean = false
|
||||
|
||||
|
|
|
@ -6,10 +6,7 @@ import android.os.Bundle
|
|||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.view.View
|
||||
import android.widget.*
|
||||
import jp.juggler.subwaytooter.api.TootApiClient
|
||||
import jp.juggler.subwaytooter.api.TootApiResult
|
||||
import jp.juggler.subwaytooter.api.TootTask
|
||||
import jp.juggler.subwaytooter.api.TootTaskRunner
|
||||
import jp.juggler.subwaytooter.api.*
|
||||
import jp.juggler.subwaytooter.api.entity.EntityId
|
||||
import jp.juggler.subwaytooter.api.entity.TootFilter
|
||||
import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||
|
@ -163,7 +160,7 @@ class ActKeywordFilter
|
|||
TootTaskRunner(this).run(account, object : TootTask {
|
||||
var filter : TootFilter? = null
|
||||
override suspend fun background(client : TootApiClient) : TootApiResult? {
|
||||
val result = client.request("${Column.PATH_FILTERS}/${filter_id}")
|
||||
val result = client.request("${ApiPath.PATH_FILTERS}/${filter_id}")
|
||||
val jsonObject = result?.jsonObject
|
||||
if(jsonObject != null) {
|
||||
filter = TootFilter(jsonObject)
|
||||
|
@ -273,12 +270,12 @@ class ActKeywordFilter
|
|||
|
||||
override suspend fun background(client : TootApiClient) = if(filter_id == null) {
|
||||
client.request(
|
||||
Column.PATH_FILTERS,
|
||||
ApiPath.PATH_FILTERS,
|
||||
params.toPostRequestBuilder()
|
||||
)
|
||||
} else {
|
||||
client.request(
|
||||
"${Column.PATH_FILTERS}/$filter_id",
|
||||
"${ApiPath.PATH_FILTERS}/$filter_id",
|
||||
params.toRequestBody().toPut()
|
||||
)
|
||||
}
|
||||
|
|
|
@ -303,7 +303,7 @@ object AppDataExporter {
|
|||
writer.name(KEY_COLUMN)
|
||||
writer.beginArray()
|
||||
for(column in app_state.columnList) {
|
||||
writer.writeJsonValue(jsonObject { column.encodeJSON(this, 0) })
|
||||
writer.writeJsonValue(jsonObject { ColumnEncoder.encode(column,this, 0) })
|
||||
}
|
||||
writer.endArray()
|
||||
}
|
||||
|
@ -319,13 +319,13 @@ object AppDataExporter {
|
|||
val item :JsonObject = reader.readJsonValue().cast() !!
|
||||
|
||||
// DB上のアカウントIDが変化したので置き換える
|
||||
when(val old_id = item.long(Column.KEY_ACCOUNT_ROW_ID) ?: - 1L) {
|
||||
when(val old_id = item.long(ColumnEncoder.KEY_ACCOUNT_ROW_ID) ?: - 1L) {
|
||||
|
||||
// 検索カラムのアカウントIDはNAアカウントと紐ついている。変換の必要はない
|
||||
- 1L -> {
|
||||
}
|
||||
|
||||
else -> item[Column.KEY_ACCOUNT_ROW_ID] = id_map[old_id]
|
||||
else -> item[ColumnEncoder.KEY_ACCOUNT_ROW_ID] = id_map[old_id]
|
||||
?: error("readColumn: can't convert account id")
|
||||
}
|
||||
|
||||
|
@ -452,7 +452,7 @@ object AppDataExporter {
|
|||
if(column == null) {
|
||||
log.e("missing column for id $id")
|
||||
} else {
|
||||
val backgroundDir = Column.getBackgroundImageDir(context)
|
||||
val backgroundDir = getBackgroundImageDir(context)
|
||||
val file =
|
||||
File(backgroundDir, "${column.column_id}:${System.currentTimeMillis()}")
|
||||
FileOutputStream(file).use { outStream ->
|
||||
|
|
|
@ -278,7 +278,7 @@ class AppState(
|
|||
columnList.mapIndexedNotNull { index, column ->
|
||||
try {
|
||||
val dst = JsonObject()
|
||||
column.encodeJSON(dst, index)
|
||||
ColumnEncoder.encode(column,dst, index)
|
||||
dst
|
||||
} catch (ex: JsonException) {
|
||||
log.trace(ex)
|
||||
|
@ -311,13 +311,13 @@ class AppState(
|
|||
|
||||
// 背景フォルダの掃除
|
||||
try {
|
||||
val backgroundImageDir = Column.getBackgroundImageDir(context)
|
||||
val backgroundImageDir = getBackgroundImageDir(context)
|
||||
backgroundImageDir.list()?.forEach { name ->
|
||||
val file = File(backgroundImageDir, name)
|
||||
if (file.isFile) {
|
||||
val delm = name.indexOf(':')
|
||||
val id = if (delm != -1) name.substring(0, delm) else name
|
||||
val column = Column.findColumnById(id)
|
||||
val column = ColumnEncoder.findColumnById(id)
|
||||
if (column == null) file.delete()
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,350 @@
|
|||
package jp.juggler.subwaytooter
|
||||
|
||||
import jp.juggler.subwaytooter.api.entity.EntityId
|
||||
import jp.juggler.subwaytooter.table.AcctColor
|
||||
import jp.juggler.util.JsonException
|
||||
import jp.juggler.util.JsonObject
|
||||
import jp.juggler.util.encodeBase64Url
|
||||
import java.lang.ref.WeakReference
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.HashMap
|
||||
|
||||
object ColumnEncoder {
|
||||
|
||||
const val KEY_ACCOUNT_ROW_ID = "account_id"
|
||||
const val KEY_TYPE = "type"
|
||||
const val KEY_COLUMN_ID = "column_id"
|
||||
const val KEY_DONT_CLOSE = "dont_close"
|
||||
private const val KEY_WITH_ATTACHMENT = "with_attachment"
|
||||
private const val KEY_WITH_HIGHLIGHT = "with_highlight"
|
||||
private const val KEY_DONT_SHOW_BOOST = "dont_show_boost"
|
||||
private const val KEY_DONT_SHOW_FAVOURITE = "dont_show_favourite"
|
||||
private const val KEY_DONT_SHOW_FOLLOW = "dont_show_follow"
|
||||
private const val KEY_DONT_SHOW_REPLY = "dont_show_reply"
|
||||
private const val KEY_DONT_SHOW_REACTION = "dont_show_reaction"
|
||||
private const val KEY_DONT_SHOW_VOTE = "dont_show_vote"
|
||||
private const val KEY_DONT_SHOW_NORMAL_TOOT = "dont_show_normal_toot"
|
||||
private const val KEY_DONT_SHOW_NON_PUBLIC_TOOT = "dont_show_non_public_toot"
|
||||
private const val KEY_DONT_STREAMING = "dont_streaming"
|
||||
private const val KEY_DONT_AUTO_REFRESH = "dont_auto_refresh"
|
||||
private const val KEY_HIDE_MEDIA_DEFAULT = "hide_media_default"
|
||||
private const val KEY_SYSTEM_NOTIFICATION_NOT_RELATED = "system_notification_not_related"
|
||||
private const val KEY_INSTANCE_LOCAL = "instance_local"
|
||||
|
||||
private const val KEY_ENABLE_SPEECH = "enable_speech"
|
||||
private const val KEY_USE_OLD_API = "use_old_api"
|
||||
private const val KEY_LAST_VIEWING_ITEM = "lastViewingItem"
|
||||
private const val KEY_QUICK_FILTER = "quickFilter"
|
||||
|
||||
private const val KEY_REGEX_TEXT = "regex_text"
|
||||
private const val KEY_LANGUAGE_FILTER = "language_filter"
|
||||
|
||||
private const val KEY_HEADER_BACKGROUND_COLOR = "header_background_color"
|
||||
private const val KEY_HEADER_TEXT_COLOR = "header_text_color"
|
||||
private const val KEY_COLUMN_BACKGROUND_COLOR = "column_background_color"
|
||||
private const val KEY_COLUMN_ACCT_TEXT_COLOR = "column_acct_text_color"
|
||||
private const val KEY_COLUMN_CONTENT_TEXT_COLOR = "column_content_text_color"
|
||||
private const val KEY_COLUMN_BACKGROUND_IMAGE = "column_background_image"
|
||||
private const val KEY_COLUMN_BACKGROUND_IMAGE_ALPHA = "column_background_image_alpha"
|
||||
|
||||
private const val KEY_PROFILE_ID = "profile_id"
|
||||
private const val KEY_PROFILE_TAB = "tab"
|
||||
private const val KEY_STATUS_ID = "status_id"
|
||||
|
||||
private const val KEY_HASHTAG = "hashtag"
|
||||
private const val KEY_HASHTAG_ANY = "hashtag_any"
|
||||
private const val KEY_HASHTAG_ALL = "hashtag_all"
|
||||
private const val KEY_HASHTAG_NONE = "hashtag_none"
|
||||
private const val KEY_HASHTAG_ACCT = "hashtag_acct"
|
||||
|
||||
private const val KEY_SEARCH_QUERY = "search_query"
|
||||
private const val KEY_SEARCH_RESOLVE = "search_resolve"
|
||||
private const val KEY_INSTANCE_URI = "instance_uri"
|
||||
|
||||
private const val KEY_REMOTE_ONLY = "remoteOnly"
|
||||
|
||||
const val KEY_COLUMN_ACCESS_ACCT = "column_access"
|
||||
const val KEY_COLUMN_ACCESS_STR = "column_access_str"
|
||||
const val KEY_COLUMN_ACCESS_COLOR = "column_access_color"
|
||||
const val KEY_COLUMN_ACCESS_COLOR_BG = "column_access_color_bg"
|
||||
const val KEY_COLUMN_NAME = "column_name"
|
||||
const val KEY_OLD_INDEX = "old_index"
|
||||
|
||||
const val KEY_ANNOUNCEMENT_HIDE_TIME = "announcementHideTime"
|
||||
|
||||
|
||||
private val columnIdMap = HashMap<String, WeakReference<Column>?>()
|
||||
|
||||
fun registerColumnId(id: String, column: Column) {
|
||||
synchronized(columnIdMap) {
|
||||
columnIdMap[id] = WeakReference(column)
|
||||
}
|
||||
}
|
||||
|
||||
fun generateColumnId(): String {
|
||||
synchronized(columnIdMap) {
|
||||
val buffer = ByteBuffer.allocate(8)
|
||||
var id = ""
|
||||
while (id.isEmpty() || columnIdMap.containsKey(id)) {
|
||||
if (id.isNotEmpty()) Thread.sleep(1L)
|
||||
buffer.clear()
|
||||
buffer.putLong(System.currentTimeMillis())
|
||||
id = buffer.array().encodeBase64Url()
|
||||
}
|
||||
columnIdMap[id] = null
|
||||
return id
|
||||
}
|
||||
}
|
||||
|
||||
fun decodeColumnId(src: JsonObject): String {
|
||||
return src.string(ColumnEncoder.KEY_COLUMN_ID) ?: generateColumnId()
|
||||
}
|
||||
|
||||
fun findColumnById(id: String): Column? {
|
||||
synchronized(columnIdMap) {
|
||||
return columnIdMap[id]?.get()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(JsonException::class)
|
||||
fun encode(column:Column,dst: JsonObject, old_index: Int) {
|
||||
column.run{
|
||||
dst[KEY_ACCOUNT_ROW_ID] = access_info.db_id
|
||||
dst[KEY_TYPE] = type.id
|
||||
dst[KEY_COLUMN_ID] = column_id
|
||||
|
||||
dst[KEY_ANNOUNCEMENT_HIDE_TIME] = announcementHideTime
|
||||
|
||||
dst.putIfTrue(KEY_DONT_CLOSE, dont_close)
|
||||
dst.putIfTrue(KEY_WITH_ATTACHMENT, with_attachment)
|
||||
dst.putIfTrue(KEY_WITH_HIGHLIGHT, with_highlight)
|
||||
dst.putIfTrue(KEY_DONT_SHOW_BOOST, dont_show_boost)
|
||||
dst.putIfTrue(KEY_DONT_SHOW_FOLLOW, dont_show_follow)
|
||||
dst.putIfTrue(KEY_DONT_SHOW_FAVOURITE, dont_show_favourite)
|
||||
dst.putIfTrue(KEY_DONT_SHOW_REPLY, dont_show_reply)
|
||||
dst.putIfTrue(KEY_DONT_SHOW_REACTION, dont_show_reaction)
|
||||
dst.putIfTrue(KEY_DONT_SHOW_VOTE, dont_show_vote)
|
||||
dst.putIfTrue(KEY_DONT_SHOW_NORMAL_TOOT, dont_show_normal_toot)
|
||||
dst.putIfTrue(KEY_DONT_SHOW_NON_PUBLIC_TOOT, dont_show_non_public_toot)
|
||||
dst.putIfTrue(KEY_DONT_STREAMING, dont_streaming)
|
||||
dst.putIfTrue(KEY_DONT_AUTO_REFRESH, dont_auto_refresh)
|
||||
dst.putIfTrue(KEY_HIDE_MEDIA_DEFAULT, hide_media_default)
|
||||
dst.putIfTrue(KEY_SYSTEM_NOTIFICATION_NOT_RELATED, system_notification_not_related)
|
||||
dst.putIfTrue(KEY_INSTANCE_LOCAL, instance_local)
|
||||
dst.putIfTrue(KEY_ENABLE_SPEECH, enable_speech)
|
||||
dst.putIfTrue(KEY_USE_OLD_API, use_old_api)
|
||||
dst[KEY_QUICK_FILTER] = quick_filter
|
||||
|
||||
last_viewing_item_id?.putTo(dst, KEY_LAST_VIEWING_ITEM)
|
||||
|
||||
dst[KEY_REGEX_TEXT] = regex_text
|
||||
|
||||
val ov = language_filter
|
||||
if (ov != null) dst[KEY_LANGUAGE_FILTER] = ov
|
||||
|
||||
dst[KEY_HEADER_BACKGROUND_COLOR] = header_bg_color
|
||||
dst[KEY_HEADER_TEXT_COLOR] = header_fg_color
|
||||
dst[KEY_COLUMN_BACKGROUND_COLOR] = column_bg_color
|
||||
dst[KEY_COLUMN_ACCT_TEXT_COLOR] = acct_color
|
||||
dst[KEY_COLUMN_CONTENT_TEXT_COLOR] = content_color
|
||||
dst[KEY_COLUMN_BACKGROUND_IMAGE] = column_bg_image
|
||||
dst[KEY_COLUMN_BACKGROUND_IMAGE_ALPHA] = column_bg_image_alpha.toDouble()
|
||||
|
||||
when (type) {
|
||||
|
||||
ColumnType.CONVERSATION,
|
||||
ColumnType.BOOSTED_BY,
|
||||
ColumnType.FAVOURITED_BY,
|
||||
ColumnType.LOCAL_AROUND,
|
||||
ColumnType.ACCOUNT_AROUND ->
|
||||
dst[KEY_STATUS_ID] = status_id.toString()
|
||||
|
||||
ColumnType.FEDERATED_AROUND -> {
|
||||
dst[KEY_STATUS_ID] = status_id.toString()
|
||||
dst[KEY_REMOTE_ONLY] = remote_only
|
||||
}
|
||||
|
||||
ColumnType.FEDERATE -> {
|
||||
dst[KEY_REMOTE_ONLY] = remote_only
|
||||
}
|
||||
|
||||
ColumnType.PROFILE -> {
|
||||
dst[KEY_PROFILE_ID] = profile_id.toString()
|
||||
dst[KEY_PROFILE_TAB] = profile_tab.id
|
||||
}
|
||||
|
||||
ColumnType.LIST_MEMBER, ColumnType.LIST_TL,
|
||||
ColumnType.MISSKEY_ANTENNA_TL -> {
|
||||
dst[KEY_PROFILE_ID] = profile_id.toString()
|
||||
}
|
||||
|
||||
ColumnType.HASHTAG -> {
|
||||
dst[KEY_HASHTAG] = hashtag
|
||||
dst[KEY_HASHTAG_ANY] = hashtag_any
|
||||
dst[KEY_HASHTAG_ALL] = hashtag_all
|
||||
dst[KEY_HASHTAG_NONE] = hashtag_none
|
||||
}
|
||||
|
||||
ColumnType.HASHTAG_FROM_ACCT -> {
|
||||
dst[KEY_HASHTAG_ACCT] = hashtag_acct
|
||||
dst[KEY_HASHTAG] = hashtag
|
||||
dst[KEY_HASHTAG_ANY] = hashtag_any
|
||||
dst[KEY_HASHTAG_ALL] = hashtag_all
|
||||
dst[KEY_HASHTAG_NONE] = hashtag_none
|
||||
}
|
||||
|
||||
ColumnType.NOTIFICATION_FROM_ACCT -> {
|
||||
dst[KEY_HASHTAG_ACCT] = hashtag_acct
|
||||
}
|
||||
|
||||
ColumnType.SEARCH -> {
|
||||
dst[KEY_SEARCH_QUERY] = search_query
|
||||
dst[KEY_SEARCH_RESOLVE] = search_resolve
|
||||
}
|
||||
|
||||
ColumnType.SEARCH_MSP, ColumnType.SEARCH_TS, ColumnType.SEARCH_NOTESTOCK -> {
|
||||
dst[KEY_SEARCH_QUERY] = search_query
|
||||
}
|
||||
|
||||
ColumnType.INSTANCE_INFORMATION -> {
|
||||
dst[KEY_INSTANCE_URI] = instance_uri
|
||||
}
|
||||
|
||||
ColumnType.PROFILE_DIRECTORY -> {
|
||||
dst[KEY_SEARCH_QUERY] = search_query
|
||||
dst[KEY_SEARCH_RESOLVE] = search_resolve
|
||||
dst[KEY_INSTANCE_URI] = instance_uri
|
||||
}
|
||||
|
||||
ColumnType.DOMAIN_TIMELINE -> {
|
||||
dst[KEY_INSTANCE_URI] = instance_uri
|
||||
}
|
||||
|
||||
else -> {
|
||||
// no extra parameter
|
||||
}
|
||||
}
|
||||
|
||||
// 以下は保存には必要ないが、カラムリスト画面で使う
|
||||
val ac = AcctColor.load(access_info)
|
||||
dst[KEY_COLUMN_ACCESS_ACCT] = access_info.acct.ascii
|
||||
dst[KEY_COLUMN_ACCESS_STR] = ac.nickname
|
||||
dst[KEY_COLUMN_ACCESS_COLOR] = ac.color_fg
|
||||
dst[KEY_COLUMN_ACCESS_COLOR_BG] = ac.color_bg
|
||||
dst[KEY_COLUMN_NAME] = getColumnName(true)
|
||||
dst[KEY_OLD_INDEX] = old_index
|
||||
}
|
||||
}
|
||||
|
||||
fun decode(column: Column, src: JsonObject) {
|
||||
column.run {
|
||||
dont_close = src.optBoolean(KEY_DONT_CLOSE)
|
||||
with_attachment = src.optBoolean(KEY_WITH_ATTACHMENT)
|
||||
with_highlight = src.optBoolean(KEY_WITH_HIGHLIGHT)
|
||||
dont_show_boost = src.optBoolean(KEY_DONT_SHOW_BOOST)
|
||||
dont_show_follow = src.optBoolean(KEY_DONT_SHOW_FOLLOW)
|
||||
dont_show_favourite = src.optBoolean(KEY_DONT_SHOW_FAVOURITE)
|
||||
dont_show_reply = src.optBoolean(KEY_DONT_SHOW_REPLY)
|
||||
dont_show_reaction = src.optBoolean(KEY_DONT_SHOW_REACTION)
|
||||
dont_show_vote = src.optBoolean(KEY_DONT_SHOW_VOTE)
|
||||
dont_show_normal_toot = src.optBoolean(KEY_DONT_SHOW_NORMAL_TOOT)
|
||||
dont_show_non_public_toot = src.optBoolean(KEY_DONT_SHOW_NON_PUBLIC_TOOT)
|
||||
dont_streaming = src.optBoolean(KEY_DONT_STREAMING)
|
||||
dont_auto_refresh = src.optBoolean(KEY_DONT_AUTO_REFRESH)
|
||||
hide_media_default = src.optBoolean(KEY_HIDE_MEDIA_DEFAULT)
|
||||
system_notification_not_related = src.optBoolean(KEY_SYSTEM_NOTIFICATION_NOT_RELATED)
|
||||
instance_local = src.optBoolean(KEY_INSTANCE_LOCAL)
|
||||
quick_filter = src.optInt(KEY_QUICK_FILTER, 0)
|
||||
|
||||
announcementHideTime = src.optLong(KEY_ANNOUNCEMENT_HIDE_TIME, 0L)
|
||||
|
||||
enable_speech = src.optBoolean(KEY_ENABLE_SPEECH)
|
||||
use_old_api = src.optBoolean(KEY_USE_OLD_API)
|
||||
last_viewing_item_id = EntityId.from(src, KEY_LAST_VIEWING_ITEM)
|
||||
|
||||
regex_text = src.string(KEY_REGEX_TEXT) ?: ""
|
||||
language_filter = src.jsonObject(KEY_LANGUAGE_FILTER)
|
||||
|
||||
header_bg_color = src.optInt(KEY_HEADER_BACKGROUND_COLOR)
|
||||
header_fg_color = src.optInt(KEY_HEADER_TEXT_COLOR)
|
||||
column_bg_color = src.optInt(KEY_COLUMN_BACKGROUND_COLOR)
|
||||
acct_color = src.optInt(KEY_COLUMN_ACCT_TEXT_COLOR)
|
||||
content_color = src.optInt(KEY_COLUMN_CONTENT_TEXT_COLOR)
|
||||
column_bg_image = src.string(KEY_COLUMN_BACKGROUND_IMAGE) ?: ""
|
||||
column_bg_image_alpha = src.optFloat(KEY_COLUMN_BACKGROUND_IMAGE_ALPHA, 1f)
|
||||
|
||||
when (type) {
|
||||
|
||||
ColumnType.CONVERSATION, ColumnType.BOOSTED_BY, ColumnType.FAVOURITED_BY,
|
||||
ColumnType.LOCAL_AROUND, ColumnType.ACCOUNT_AROUND ->
|
||||
status_id = EntityId.mayNull(src.string(KEY_STATUS_ID))
|
||||
|
||||
ColumnType.FEDERATED_AROUND -> {
|
||||
status_id = EntityId.mayNull(src.string(KEY_STATUS_ID))
|
||||
remote_only = src.optBoolean(KEY_REMOTE_ONLY, false)
|
||||
}
|
||||
|
||||
ColumnType.FEDERATE -> {
|
||||
remote_only = src.optBoolean(KEY_REMOTE_ONLY, false)
|
||||
}
|
||||
|
||||
ColumnType.PROFILE -> {
|
||||
profile_id = EntityId.mayNull(src.string(KEY_PROFILE_ID))
|
||||
val tabId = src.optInt(KEY_PROFILE_TAB)
|
||||
profile_tab = ProfileTab.values().find { it.id == tabId } ?: ProfileTab.Status
|
||||
}
|
||||
|
||||
ColumnType.LIST_MEMBER, ColumnType.LIST_TL,
|
||||
ColumnType.MISSKEY_ANTENNA_TL -> {
|
||||
profile_id = EntityId.mayNull(src.string(KEY_PROFILE_ID))
|
||||
}
|
||||
|
||||
ColumnType.HASHTAG -> {
|
||||
hashtag = src.optString(KEY_HASHTAG)
|
||||
hashtag_any = src.optString(KEY_HASHTAG_ANY)
|
||||
hashtag_all = src.optString(KEY_HASHTAG_ALL)
|
||||
hashtag_none = src.optString(KEY_HASHTAG_NONE)
|
||||
}
|
||||
|
||||
ColumnType.HASHTAG_FROM_ACCT -> {
|
||||
hashtag_acct = src.optString(KEY_HASHTAG_ACCT)
|
||||
hashtag = src.optString(KEY_HASHTAG)
|
||||
hashtag_any = src.optString(KEY_HASHTAG_ANY)
|
||||
hashtag_all = src.optString(KEY_HASHTAG_ALL)
|
||||
hashtag_none = src.optString(KEY_HASHTAG_NONE)
|
||||
}
|
||||
|
||||
ColumnType.NOTIFICATION_FROM_ACCT -> {
|
||||
hashtag_acct = src.optString(KEY_HASHTAG_ACCT)
|
||||
}
|
||||
|
||||
ColumnType.SEARCH -> {
|
||||
search_query = src.optString(KEY_SEARCH_QUERY)
|
||||
search_resolve = src.optBoolean(KEY_SEARCH_RESOLVE, false)
|
||||
}
|
||||
|
||||
ColumnType.SEARCH_MSP, ColumnType.SEARCH_TS, ColumnType.SEARCH_NOTESTOCK -> search_query =
|
||||
src.optString(KEY_SEARCH_QUERY)
|
||||
|
||||
ColumnType.INSTANCE_INFORMATION -> instance_uri = src.optString(KEY_INSTANCE_URI)
|
||||
|
||||
ColumnType.PROFILE_DIRECTORY -> {
|
||||
instance_uri = src.optString(KEY_INSTANCE_URI)
|
||||
search_query = src.optString(KEY_SEARCH_QUERY)
|
||||
search_resolve = src.optBoolean(KEY_SEARCH_RESOLVE, false)
|
||||
}
|
||||
|
||||
ColumnType.DOMAIN_TIMELINE -> {
|
||||
instance_uri = src.optString(KEY_INSTANCE_URI)
|
||||
}
|
||||
|
||||
else -> {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -55,51 +55,6 @@ fun Column.setHeaderBackground(view: View) {
|
|||
)
|
||||
}
|
||||
|
||||
// マストドン2.4.3rcのキーワードフィルタのコンテキスト
|
||||
fun Column.getFilterContext() = when (type) {
|
||||
|
||||
ColumnType.HOME, ColumnType.LIST_TL, ColumnType.MISSKEY_HYBRID -> TootFilter.CONTEXT_HOME
|
||||
|
||||
ColumnType.NOTIFICATIONS, ColumnType.NOTIFICATION_FROM_ACCT -> TootFilter.CONTEXT_NOTIFICATIONS
|
||||
|
||||
ColumnType.CONVERSATION -> TootFilter.CONTEXT_THREAD
|
||||
|
||||
ColumnType.DIRECT_MESSAGES -> TootFilter.CONTEXT_THREAD
|
||||
|
||||
ColumnType.PROFILE -> TootFilter.CONTEXT_PROFILE
|
||||
|
||||
else -> TootFilter.CONTEXT_PUBLIC
|
||||
// ColumnType.MISSKEY_HYBRID や ColumnType.MISSKEY_ANTENNA_TL はHOMEでもPUBLICでもある…
|
||||
// Misskeyだし関係ないが、NONEにするとアプリ内で完結するフィルタも働かなくなる
|
||||
}
|
||||
|
||||
fun Column.onFiltersChanged2(filterList: ArrayList<TootFilter>) {
|
||||
val newFilter = encodeFilterTree(filterList) ?: return
|
||||
this.keywordFilterTrees = newFilter
|
||||
checkFiltersForListData(newFilter)
|
||||
}
|
||||
|
||||
fun Column.onFilterDeleted(filter: TootFilter, filterList: ArrayList<TootFilter>) {
|
||||
if (type == ColumnType.KEYWORD_FILTER) {
|
||||
val tmp_list = ArrayList<TimelineItem>(list_data.size)
|
||||
for (o in list_data) {
|
||||
if (o is TootFilter) {
|
||||
if (o.id == filter.id) continue
|
||||
}
|
||||
tmp_list.add(o)
|
||||
}
|
||||
if (tmp_list.size != list_data.size) {
|
||||
list_data.clear()
|
||||
list_data.addAll(tmp_list)
|
||||
fireShowContent(reason = "onFilterDeleted")
|
||||
}
|
||||
} else {
|
||||
val context = getFilterContext()
|
||||
if (context != TootFilter.CONTEXT_NONE) {
|
||||
onFiltersChanged2(filterList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Column.onScheduleDeleted(item: TootScheduled) {
|
||||
val tmp_list = ArrayList<TimelineItem>(list_data.size)
|
||||
|
@ -114,62 +69,12 @@ fun Column.onScheduleDeleted(item: TootScheduled) {
|
|||
}
|
||||
}
|
||||
|
||||
// カラム設定に正規表現フィルタを含めるなら真
|
||||
fun Column.canStatusFilter(): Boolean {
|
||||
if (getFilterContext() != TootFilter.CONTEXT_NONE) return true
|
||||
|
||||
return when (type) {
|
||||
ColumnType.SEARCH_MSP, ColumnType.SEARCH_TS, ColumnType.SEARCH_NOTESTOCK -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
// カラム設定に「すべての画像を隠す」ボタンを含めるなら真
|
||||
fun Column.canNSFWDefault(): Boolean = canStatusFilter()
|
||||
|
||||
fun Column.canRemoteOnly() = when (type) {
|
||||
ColumnType.FEDERATE, ColumnType.FEDERATED_AROUND -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
// カラム設定に「ブーストを表示しない」ボタンを含めるなら真
|
||||
fun Column.canFilterBoost(): Boolean = when (type) {
|
||||
ColumnType.HOME, ColumnType.MISSKEY_HYBRID, ColumnType.PROFILE,
|
||||
ColumnType.NOTIFICATIONS, ColumnType.NOTIFICATION_FROM_ACCT,
|
||||
ColumnType.LIST_TL, ColumnType.MISSKEY_ANTENNA_TL -> true
|
||||
ColumnType.LOCAL, ColumnType.FEDERATE, ColumnType.HASHTAG, ColumnType.SEARCH -> isMisskey
|
||||
ColumnType.HASHTAG_FROM_ACCT -> false
|
||||
ColumnType.CONVERSATION, ColumnType.DIRECT_MESSAGES -> isMisskey
|
||||
else -> false
|
||||
}
|
||||
|
||||
// カラム設定に「返信を表示しない」ボタンを含めるなら真
|
||||
fun Column.canFilterReply(): Boolean = when (type) {
|
||||
ColumnType.HOME, ColumnType.MISSKEY_HYBRID, ColumnType.PROFILE,
|
||||
ColumnType.NOTIFICATIONS, ColumnType.NOTIFICATION_FROM_ACCT,
|
||||
ColumnType.LIST_TL, ColumnType.MISSKEY_ANTENNA_TL, ColumnType.DIRECT_MESSAGES -> true
|
||||
ColumnType.LOCAL, ColumnType.FEDERATE, ColumnType.HASHTAG, ColumnType.SEARCH -> isMisskey
|
||||
ColumnType.HASHTAG_FROM_ACCT -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
fun Column.canFilterNormalToot(): Boolean = when (type) {
|
||||
ColumnType.NOTIFICATIONS -> true
|
||||
ColumnType.HOME, ColumnType.MISSKEY_HYBRID,
|
||||
ColumnType.LIST_TL, ColumnType.MISSKEY_ANTENNA_TL -> true
|
||||
ColumnType.LOCAL, ColumnType.FEDERATE, ColumnType.HASHTAG, ColumnType.SEARCH -> isMisskey
|
||||
ColumnType.HASHTAG_FROM_ACCT -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
fun Column.canFilterNonPublicToot(): Boolean = when (type) {
|
||||
ColumnType.HOME, ColumnType.MISSKEY_HYBRID,
|
||||
ColumnType.LIST_TL, ColumnType.MISSKEY_ANTENNA_TL -> true
|
||||
ColumnType.LOCAL, ColumnType.FEDERATE, ColumnType.HASHTAG, ColumnType.SEARCH -> isMisskey
|
||||
ColumnType.HASHTAG_FROM_ACCT -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
fun Column.canReloadWhenRefreshTop(): Boolean = when (type) {
|
||||
|
||||
ColumnType.KEYWORD_FILTER,
|
||||
|
@ -806,9 +711,6 @@ fun Column.onHideFavouriteNotification(acct: Acct) {
|
|||
}
|
||||
}
|
||||
|
||||
fun Column.onLanguageFilterChanged() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
|
||||
// ステータスが削除された時に呼ばれる
|
||||
|
@ -882,274 +784,3 @@ fun replaceConversationSummary(
|
|||
if (removeSet.contains(o.id)) it.remove()
|
||||
}
|
||||
}
|
||||
|
||||
fun Column.initFilter() {
|
||||
column_regex_filter = Column.COLUMN_REGEX_FILTER_DEFAULT
|
||||
val regex_text = this.regex_text
|
||||
if (regex_text.isNotEmpty()) {
|
||||
try {
|
||||
val re = Pattern.compile(regex_text)
|
||||
column_regex_filter =
|
||||
{ text: CharSequence? ->
|
||||
if (text?.isEmpty() != false)
|
||||
false
|
||||
else
|
||||
re.matcher(text).find()
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
Column.log.trace(ex)
|
||||
}
|
||||
}
|
||||
|
||||
favMuteSet = FavMute.acctSet
|
||||
highlight_trie = HighlightWord.nameSet
|
||||
}
|
||||
|
||||
private fun Column.isFilteredByAttachment(status: TootStatus): Boolean {
|
||||
// オプションがどれも設定されていないならフィルタしない(false)
|
||||
if (!(with_attachment || with_highlight)) return false
|
||||
|
||||
val matchMedia = with_attachment && status.reblog?.hasMedia() ?: status.hasMedia()
|
||||
val matchHighlight =
|
||||
with_highlight && null != (status.reblog?.highlightAny ?: status.highlightAny)
|
||||
|
||||
// どれかの条件を満たすならフィルタしない(false)、どれも満たさないならフィルタする(true)
|
||||
return !(matchMedia || matchHighlight)
|
||||
}
|
||||
|
||||
internal fun Column.isFiltered(status: TootStatus): Boolean {
|
||||
|
||||
val filterTrees = keywordFilterTrees
|
||||
if (filterTrees != null) {
|
||||
if (status.isKeywordFiltered(access_info, filterTrees.treeIrreversible)) {
|
||||
Column.log.d("status filtered by treeIrreversible")
|
||||
return true
|
||||
}
|
||||
|
||||
// just update _filtered flag for reversible filter
|
||||
status.updateKeywordFilteredFlag(access_info, filterTrees)
|
||||
}
|
||||
|
||||
if (isFilteredByAttachment(status)) return true
|
||||
|
||||
val reblog = status.reblog
|
||||
|
||||
if (dont_show_boost) {
|
||||
if (reblog != null) return true
|
||||
}
|
||||
|
||||
if (dont_show_reply) {
|
||||
if (status.in_reply_to_id != null) return true
|
||||
if (reblog?.in_reply_to_id != null) return true
|
||||
}
|
||||
|
||||
if (dont_show_normal_toot) {
|
||||
if (status.in_reply_to_id == null && reblog == null) return true
|
||||
}
|
||||
if (dont_show_non_public_toot) {
|
||||
if (!status.visibility.isPublic) return true
|
||||
}
|
||||
|
||||
if (column_regex_filter(status.decoded_content)) return true
|
||||
if (column_regex_filter(reblog?.decoded_content)) return true
|
||||
if (column_regex_filter(status.decoded_spoiler_text)) return true
|
||||
if (column_regex_filter(reblog?.decoded_spoiler_text)) return true
|
||||
|
||||
if (checkLanguageFilter(status)) return true
|
||||
|
||||
if (access_info.isPseudo) {
|
||||
var r = UserRelation.loadPseudo(access_info.getFullAcct(status.account))
|
||||
if (r.muting || r.blocking) return true
|
||||
if (reblog != null) {
|
||||
r = UserRelation.loadPseudo(access_info.getFullAcct(reblog.account))
|
||||
if (r.muting || r.blocking) return true
|
||||
}
|
||||
}
|
||||
|
||||
return status.checkMuted()
|
||||
}
|
||||
|
||||
// true if the status will be hidden
|
||||
private fun Column.checkLanguageFilter(status: TootStatus?): Boolean {
|
||||
status ?: return false
|
||||
val languageFilter = language_filter ?: return false
|
||||
|
||||
val allow = languageFilter.boolean(
|
||||
status.language ?: status.reblog?.language ?: TootStatus.LANGUAGE_CODE_UNKNOWN
|
||||
)
|
||||
?: languageFilter.boolean(TootStatus.LANGUAGE_CODE_DEFAULT)
|
||||
?: true
|
||||
|
||||
return !allow
|
||||
}
|
||||
|
||||
internal fun Column.isFiltered(item: TootNotification): Boolean {
|
||||
|
||||
if (when (quick_filter) {
|
||||
Column.QUICK_FILTER_ALL -> when (item.type) {
|
||||
TootNotification.TYPE_FAVOURITE -> dont_show_favourite
|
||||
|
||||
TootNotification.TYPE_REBLOG,
|
||||
TootNotification.TYPE_RENOTE,
|
||||
TootNotification.TYPE_QUOTE -> dont_show_boost
|
||||
|
||||
TootNotification.TYPE_FOLLOW,
|
||||
TootNotification.TYPE_UNFOLLOW,
|
||||
TootNotification.TYPE_FOLLOW_REQUEST,
|
||||
TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY,
|
||||
TootNotification.TYPE_FOLLOW_REQUEST_ACCEPTED_MISSKEY -> dont_show_follow
|
||||
|
||||
TootNotification.TYPE_MENTION,
|
||||
TootNotification.TYPE_REPLY -> dont_show_reply
|
||||
|
||||
TootNotification.TYPE_EMOJI_REACTION,
|
||||
TootNotification.TYPE_REACTION -> dont_show_reaction
|
||||
|
||||
TootNotification.TYPE_VOTE,
|
||||
TootNotification.TYPE_POLL,
|
||||
TootNotification.TYPE_POLL_VOTE_MISSKEY -> dont_show_vote
|
||||
|
||||
TootNotification.TYPE_STATUS -> dont_show_normal_toot
|
||||
else -> false
|
||||
}
|
||||
|
||||
else -> when (item.type) {
|
||||
TootNotification.TYPE_FAVOURITE -> quick_filter != Column.QUICK_FILTER_FAVOURITE
|
||||
TootNotification.TYPE_REBLOG,
|
||||
TootNotification.TYPE_RENOTE,
|
||||
TootNotification.TYPE_QUOTE -> quick_filter != Column.QUICK_FILTER_BOOST
|
||||
|
||||
TootNotification.TYPE_FOLLOW,
|
||||
TootNotification.TYPE_UNFOLLOW,
|
||||
TootNotification.TYPE_FOLLOW_REQUEST,
|
||||
TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY,
|
||||
TootNotification.TYPE_FOLLOW_REQUEST_ACCEPTED_MISSKEY -> quick_filter != Column.QUICK_FILTER_FOLLOW
|
||||
|
||||
TootNotification.TYPE_MENTION,
|
||||
TootNotification.TYPE_REPLY -> quick_filter != Column.QUICK_FILTER_MENTION
|
||||
|
||||
TootNotification.TYPE_EMOJI_REACTION,
|
||||
TootNotification.TYPE_REACTION -> quick_filter != Column.QUICK_FILTER_REACTION
|
||||
|
||||
TootNotification.TYPE_VOTE,
|
||||
TootNotification.TYPE_POLL,
|
||||
TootNotification.TYPE_POLL_VOTE_MISSKEY -> quick_filter != Column.QUICK_FILTER_VOTE
|
||||
|
||||
TootNotification.TYPE_STATUS -> quick_filter != Column.QUICK_FILTER_POST
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
) {
|
||||
Column.log.d("isFiltered: ${item.type} notification filtered.")
|
||||
return true
|
||||
}
|
||||
|
||||
val status = item.status
|
||||
val filterTrees = keywordFilterTrees
|
||||
if (status != null && filterTrees != null) {
|
||||
if (status.isKeywordFiltered(access_info, filterTrees.treeIrreversible)) {
|
||||
Column.log.d("isFiltered: status muted by treeIrreversible.")
|
||||
return true
|
||||
}
|
||||
|
||||
// just update _filtered flag for reversible filter
|
||||
status.updateKeywordFilteredFlag(access_info, filterTrees)
|
||||
}
|
||||
if (checkLanguageFilter(status)) return true
|
||||
|
||||
if (status?.checkMuted() == true) {
|
||||
Column.log.d("isFiltered: status muted by in-app muted words.")
|
||||
return true
|
||||
}
|
||||
|
||||
// ふぁぼ魔ミュート
|
||||
when (item.type) {
|
||||
TootNotification.TYPE_REBLOG,
|
||||
TootNotification.TYPE_RENOTE,
|
||||
TootNotification.TYPE_QUOTE,
|
||||
TootNotification.TYPE_FAVOURITE,
|
||||
TootNotification.TYPE_EMOJI_REACTION,
|
||||
TootNotification.TYPE_REACTION,
|
||||
TootNotification.TYPE_FOLLOW,
|
||||
TootNotification.TYPE_FOLLOW_REQUEST,
|
||||
TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY,
|
||||
TootNotification.TYPE_FOLLOW_REQUEST_ACCEPTED_MISSKEY -> {
|
||||
val who = item.account
|
||||
if (who != null && favMuteSet?.contains(access_info.getFullAcct(who)) == true) {
|
||||
Column.log.d("%s is in favMuteSet.", access_info.getFullAcct(who))
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// フィルタを読み直してリストを返す。またはnull
|
||||
internal suspend fun Column.loadFilter2(client: TootApiClient): ArrayList<TootFilter>? {
|
||||
if (access_info.isPseudo || access_info.isMisskey) return null
|
||||
val column_context = getFilterContext()
|
||||
if (column_context == 0) return null
|
||||
val result = client.request(Column.PATH_FILTERS)
|
||||
|
||||
val jsonArray = result?.jsonArray ?: return null
|
||||
return TootFilter.parseList(jsonArray)
|
||||
}
|
||||
|
||||
internal fun Column.encodeFilterTree(filterList: ArrayList<TootFilter>?): FilterTrees? {
|
||||
val column_context = getFilterContext()
|
||||
if (column_context == 0 || filterList == null) return null
|
||||
val result = FilterTrees()
|
||||
val now = System.currentTimeMillis()
|
||||
for (filter in filterList) {
|
||||
if (filter.time_expires_at > 0L && now >= filter.time_expires_at) continue
|
||||
if ((filter.context and column_context) != 0) {
|
||||
|
||||
val validator = when (filter.whole_word) {
|
||||
true -> WordTrieTree.WORD_VALIDATOR
|
||||
else -> WordTrieTree.EMPTY_VALIDATOR
|
||||
}
|
||||
|
||||
if (filter.irreversible) {
|
||||
result.treeIrreversible
|
||||
} else {
|
||||
result.treeReversible
|
||||
}.add(filter.phrase, validator = validator)
|
||||
|
||||
result.treeAll.add(filter.phrase, validator = validator)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun Column.checkFiltersForListData(trees: FilterTrees?) {
|
||||
trees ?: return
|
||||
|
||||
val changeList = ArrayList<AdapterChange>()
|
||||
list_data.forEachIndexed { idx, item ->
|
||||
when (item) {
|
||||
is TootStatus -> {
|
||||
val old_filtered = item.filtered
|
||||
item.updateKeywordFilteredFlag(access_info, trees, checkIrreversible = true)
|
||||
if (old_filtered != item.filtered) {
|
||||
changeList.add(AdapterChange(AdapterChangeType.RangeChange, idx))
|
||||
}
|
||||
}
|
||||
|
||||
is TootNotification -> {
|
||||
val s = item.status
|
||||
if (s != null) {
|
||||
val old_filtered = s.filtered
|
||||
s.updateKeywordFilteredFlag(access_info, trees, checkIrreversible = true)
|
||||
if (old_filtered != s.filtered) {
|
||||
changeList.add(AdapterChange(AdapterChangeType.RangeChange, idx))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fireShowContent(reason = "filter updated", changeList = changeList)
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,582 @@
|
|||
package jp.juggler.subwaytooter
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Environment
|
||||
import android.os.SystemClock
|
||||
import jp.juggler.subwaytooter.api.*
|
||||
import jp.juggler.subwaytooter.api.entity.*
|
||||
import jp.juggler.subwaytooter.notification.PollingWorker
|
||||
import jp.juggler.subwaytooter.streaming.streamSpec
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.subwaytooter.util.ScrollPosition
|
||||
import jp.juggler.util.findOrNull
|
||||
import jp.juggler.util.groupEx
|
||||
import jp.juggler.util.isMainThread
|
||||
import java.io.File
|
||||
import kotlin.math.max
|
||||
|
||||
fun Column.getIconId(): Int = type.iconId(access_info.acct)
|
||||
|
||||
fun Column.getColumnName(long: Boolean) =
|
||||
type.name2(this, long) ?: type.name1(context)
|
||||
|
||||
|
||||
|
||||
fun Column.addColumnViewHolder(cvh: ColumnViewHolder) {
|
||||
|
||||
// 現在のリストにあるなら削除する
|
||||
removeColumnViewHolder(cvh)
|
||||
|
||||
// 最後に追加されたものが先頭にくるようにする
|
||||
// 呼び出しの後に必ず追加されているようにする
|
||||
_holder_list.addFirst(cvh)
|
||||
}
|
||||
|
||||
fun Column.removeColumnViewHolder(cvh: ColumnViewHolder) {
|
||||
val it = _holder_list.iterator()
|
||||
while (it.hasNext()) {
|
||||
if (cvh == it.next()) it.remove()
|
||||
}
|
||||
}
|
||||
|
||||
fun Column.removeColumnViewHolderByActivity(activity: ActMain) {
|
||||
val it = _holder_list.iterator()
|
||||
while (it.hasNext()) {
|
||||
val cvh = it.next()
|
||||
if (cvh.activity == activity) {
|
||||
it.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Column.hasMultipleViewHolder(): Boolean = _holder_list.size > 1
|
||||
|
||||
fun Column.fireShowContent(
|
||||
reason: String,
|
||||
changeList: List<AdapterChange>? = null,
|
||||
reset: Boolean = false
|
||||
) {
|
||||
if (!isMainThread) {
|
||||
throw RuntimeException("fireShowContent: not on main thread.")
|
||||
}
|
||||
viewHolder?.showContent(reason, changeList, reset)
|
||||
}
|
||||
|
||||
fun Column.fireShowColumnHeader() {
|
||||
if (!isMainThread) {
|
||||
throw RuntimeException("fireShowColumnHeader: not on main thread.")
|
||||
}
|
||||
viewHolder?.showColumnHeader()
|
||||
}
|
||||
|
||||
fun Column.fireShowColumnStatus() {
|
||||
if (!isMainThread) {
|
||||
throw RuntimeException("fireShowColumnStatus: not on main thread.")
|
||||
}
|
||||
viewHolder?.showColumnStatus()
|
||||
}
|
||||
|
||||
fun Column.fireColumnColor() {
|
||||
if (!isMainThread) {
|
||||
throw RuntimeException("fireColumnColor: not on main thread.")
|
||||
}
|
||||
viewHolder?.showColumnColor()
|
||||
}
|
||||
|
||||
fun Column.fireRelativeTime() {
|
||||
if (!isMainThread) {
|
||||
throw RuntimeException("fireRelativeTime: not on main thread.")
|
||||
}
|
||||
viewHolder?.updateRelativeTime()
|
||||
}
|
||||
|
||||
fun Column.fireRebindAdapterItems() {
|
||||
if (!isMainThread) {
|
||||
throw RuntimeException("fireRelativeTime: not on main thread.")
|
||||
}
|
||||
viewHolder?.rebindAdapterItems()
|
||||
}
|
||||
|
||||
fun Column.cancelLastTask() {
|
||||
if (lastTask != null) {
|
||||
lastTask?.cancel()
|
||||
lastTask = null
|
||||
//
|
||||
bInitialLoading = false
|
||||
bRefreshLoading = false
|
||||
mInitialLoadingError = context.getString(R.string.cancelled)
|
||||
}
|
||||
}
|
||||
|
||||
// @Nullable String parseMaxId( TootApiResult result ){
|
||||
// if( result != null && result.link_older != null ){
|
||||
// Matcher m = reMaxId.matcher( result.link_older );
|
||||
// if( m.get() ) return m.group( 1 );
|
||||
// }
|
||||
// return null;
|
||||
// }
|
||||
|
||||
//
|
||||
suspend fun Column.updateRelation(
|
||||
client: TootApiClient,
|
||||
list: ArrayList<TimelineItem>?,
|
||||
whoRef: TootAccountRef?,
|
||||
parser: TootParser
|
||||
) {
|
||||
if (access_info.isPseudo) return
|
||||
|
||||
val env = UpdateRelationEnv(this)
|
||||
|
||||
env.add(whoRef)
|
||||
|
||||
list?.forEach {
|
||||
when (it) {
|
||||
is TootAccountRef -> env.add(it)
|
||||
is TootStatus -> env.add(it)
|
||||
is TootNotification -> env.add(it)
|
||||
is TootConversationSummary -> env.add(it.last_status)
|
||||
}
|
||||
}
|
||||
env.update(client, parser)
|
||||
}
|
||||
|
||||
fun Column.parseRange(
|
||||
result: TootApiResult?,
|
||||
list: List<TimelineItem>?
|
||||
): Pair<EntityId?, EntityId?> {
|
||||
var idMin: EntityId? = null
|
||||
var idMax: EntityId? = null
|
||||
|
||||
if (isMisskey && list != null) {
|
||||
// MisskeyはLinkヘッダがないので、常にデータからIDを読む
|
||||
|
||||
for (item in list) {
|
||||
// injectされたデータをデータ範囲に追加しない
|
||||
if (item.isInjected()) continue
|
||||
|
||||
val id = item.getOrderId()
|
||||
if (id.notDefaultOrConfirming) {
|
||||
if (idMin == null || id < idMin) idMin = id
|
||||
if (idMax == null || id > idMax) idMax = id
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Linkヘッダを読む
|
||||
idMin = Column.reMaxId.matcher(result?.link_older ?: "").findOrNull()
|
||||
?.let {
|
||||
EntityId(it.groupEx(1)!!)
|
||||
}
|
||||
|
||||
idMax = Column.reMinId.matcher(result?.link_newer ?: "").findOrNull()
|
||||
?.let {
|
||||
// min_idとsince_idの読み分けは現在利用してない it.groupEx(1)=="min_id"
|
||||
EntityId(it.groupEx(2)!!)
|
||||
}
|
||||
}
|
||||
|
||||
return Pair(idMin, idMax)
|
||||
}
|
||||
// int scroll_hack;
|
||||
|
||||
// return true if list bottom may have unread remain
|
||||
fun Column.saveRange(
|
||||
bBottom: Boolean,
|
||||
bTop: Boolean,
|
||||
result: TootApiResult?,
|
||||
list: List<TimelineItem>?
|
||||
): Boolean {
|
||||
val (idMin, idMax) = parseRange(result, list)
|
||||
|
||||
var hasBottomRemain = false
|
||||
|
||||
if (bBottom) when (idMin) {
|
||||
null -> idOld = null // リストの終端
|
||||
else -> {
|
||||
val i = idOld?.compareTo(idMin)
|
||||
if (i == null || i > 0) {
|
||||
idOld = idMin
|
||||
hasBottomRemain = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bTop) when (idMax) {
|
||||
null -> {
|
||||
// リロードを許容するため、取得内容がカラでもidRecentを変更しない
|
||||
}
|
||||
|
||||
else -> {
|
||||
val i = idRecent?.compareTo(idMax)
|
||||
if (i == null || i < 0) {
|
||||
idRecent = idMax
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hasBottomRemain
|
||||
}
|
||||
|
||||
// return true if list bottom may have unread remain
|
||||
fun Column.saveRangeBottom(result: TootApiResult?, list: List<TimelineItem>?) =
|
||||
saveRange(true, bTop = false, result = result, list = list)
|
||||
|
||||
// return true if list bottom may have unread remain
|
||||
fun Column.saveRangeTop(result: TootApiResult?, list: List<TimelineItem>?) =
|
||||
saveRange(false, bTop = true, result = result, list = list)
|
||||
|
||||
fun Column.addRange(
|
||||
bBottom: Boolean,
|
||||
path: String,
|
||||
delimiter: Char = if (-1 == path.indexOf('?')) '?' else '&'
|
||||
) = if (bBottom) {
|
||||
if (idOld != null) "$path${delimiter}max_id=${idOld}" else path
|
||||
} else {
|
||||
if (idRecent != null) "$path${delimiter}since_id=${idRecent}" else path
|
||||
}
|
||||
|
||||
fun Column.addRangeMin(
|
||||
path: String,
|
||||
delimiter: Char = if (-1 != path.indexOf('?')) '&' else '?'
|
||||
) = if (idRecent == null) path else "$path${delimiter}min_id=${idRecent}"
|
||||
|
||||
fun Column.toAdapterIndex(listIndex: Int): Int {
|
||||
return if (type.headerType != null) listIndex + 1 else listIndex
|
||||
}
|
||||
|
||||
fun Column.toListIndex(adapterIndex: Int): Int {
|
||||
return if (type.headerType != null) adapterIndex - 1 else adapterIndex
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
// Streaming
|
||||
|
||||
fun Column.onStart() {
|
||||
|
||||
// 破棄されたカラムなら何もしない
|
||||
if (is_dispose.get()) {
|
||||
Column.log.d("onStart: column was disposed.")
|
||||
return
|
||||
}
|
||||
|
||||
// 未初期化なら何もしない
|
||||
if (!bFirstInitialized) {
|
||||
Column.log.d("onStart: column is not initialized.")
|
||||
return
|
||||
}
|
||||
|
||||
// 初期ロード中なら何もしない
|
||||
if (bInitialLoading) {
|
||||
Column.log.d("onStart: column is in initial loading.")
|
||||
return
|
||||
}
|
||||
|
||||
// フィルタ一覧のリロードが必要
|
||||
if (filter_reload_required) {
|
||||
filter_reload_required = false
|
||||
startLoading()
|
||||
return
|
||||
}
|
||||
|
||||
// 始端リフレッシュの最中だった
|
||||
// リフレッシュ終了時に自動でストリーミング開始するはず
|
||||
if (bRefreshingTop) {
|
||||
Column.log.d("onStart: bRefreshingTop is true.")
|
||||
return
|
||||
}
|
||||
|
||||
if (!bRefreshLoading
|
||||
&& canAutoRefresh()
|
||||
&& !Pref.bpDontRefreshOnResume(app_state.pref)
|
||||
&& !dont_auto_refresh
|
||||
) {
|
||||
// リフレッシュしてからストリーミング開始
|
||||
Column.log.d("onStart: start auto refresh.")
|
||||
startRefresh(bSilent = true, bBottom = false)
|
||||
} else if (isSearchColumn) {
|
||||
// 検索カラムはリフレッシュもストリーミングもないが、表示開始のタイミングでリストの再描画を行いたい
|
||||
fireShowContent(reason = "Column onStart isSearchColumn", reset = true)
|
||||
} else if (canStartStreaming() && streamSpec != null) {
|
||||
// ギャップつきでストリーミング開始
|
||||
this.bPutGap = true
|
||||
fireShowColumnStatus()
|
||||
}
|
||||
}
|
||||
|
||||
fun Column.saveScrollPosition() {
|
||||
try {
|
||||
if (viewHolder?.saveScrollPosition() == true) {
|
||||
val ss = this.scroll_save
|
||||
if (ss != null) {
|
||||
val idx = toListIndex(ss.adapterIndex)
|
||||
if (0 <= idx && idx < list_data.size) {
|
||||
val item = list_data[idx]
|
||||
this.last_viewing_item_id = item.getOrderId()
|
||||
// とりあえず保存はするが
|
||||
// TLデータそのものを永続化しないかぎり出番はないっぽい
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
Column.log.e(ex, "can't get last_viewing_item_id.")
|
||||
}
|
||||
}
|
||||
|
||||
fun Column.getNotificationTypeString(): String {
|
||||
val sb = StringBuilder()
|
||||
sb.append("(")
|
||||
|
||||
when (quick_filter) {
|
||||
Column.QUICK_FILTER_ALL -> {
|
||||
var n = 0
|
||||
if (!dont_show_reply) {
|
||||
if (n++ > 0) sb.append(", ")
|
||||
sb.append(context.getString(R.string.notification_type_mention))
|
||||
}
|
||||
if (!dont_show_follow) {
|
||||
if (n++ > 0) sb.append(", ")
|
||||
sb.append(context.getString(R.string.notification_type_follow))
|
||||
}
|
||||
if (!dont_show_boost) {
|
||||
if (n++ > 0) sb.append(", ")
|
||||
sb.append(context.getString(R.string.notification_type_boost))
|
||||
}
|
||||
if (!dont_show_favourite) {
|
||||
if (n++ > 0) sb.append(", ")
|
||||
sb.append(context.getString(R.string.notification_type_favourite))
|
||||
}
|
||||
if (isMisskey && !dont_show_reaction) {
|
||||
if (n++ > 0) sb.append(", ")
|
||||
sb.append(context.getString(R.string.notification_type_reaction))
|
||||
}
|
||||
if (!dont_show_vote) {
|
||||
if (n++ > 0) sb.append(", ")
|
||||
sb.append(context.getString(R.string.notification_type_vote))
|
||||
}
|
||||
val n_max = if (isMisskey) {
|
||||
6
|
||||
} else {
|
||||
5
|
||||
}
|
||||
if (n == 0 || n == n_max) return "" // 全部か皆無なら部分表記は要らない
|
||||
}
|
||||
|
||||
Column.QUICK_FILTER_MENTION -> sb.append(context.getString(R.string.notification_type_mention))
|
||||
Column.QUICK_FILTER_FAVOURITE -> sb.append(context.getString(R.string.notification_type_favourite))
|
||||
Column.QUICK_FILTER_BOOST -> sb.append(context.getString(R.string.notification_type_boost))
|
||||
Column.QUICK_FILTER_FOLLOW -> sb.append(context.getString(R.string.notification_type_follow))
|
||||
Column.QUICK_FILTER_REACTION -> sb.append(context.getString(R.string.notification_type_reaction))
|
||||
Column.QUICK_FILTER_VOTE -> sb.append(context.getString(R.string.notification_type_vote))
|
||||
Column.QUICK_FILTER_POST -> sb.append(context.getString(R.string.notification_type_post))
|
||||
}
|
||||
|
||||
sb.append(")")
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
fun Column.mergeStreamingMessage() {
|
||||
val handler = app_state.handler
|
||||
|
||||
// 未初期化や初期ロード中ならキューをクリアして何もしない
|
||||
if (!canHandleStreamingMessage()) {
|
||||
stream_data_queue.clear()
|
||||
handler.removeCallbacks(procMergeStreamingMessage)
|
||||
return
|
||||
}
|
||||
|
||||
// 前回マージしてから暫くは待機してリトライ
|
||||
// カラムがビジー状態なら待機してリトライ
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
var remain = last_show_stream_data.get() + 333L - now
|
||||
if (bRefreshLoading) remain = max(333L, remain)
|
||||
if (remain > 0) {
|
||||
handler.removeCallbacks(procMergeStreamingMessage)
|
||||
handler.postDelayed(procMergeStreamingMessage, 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(procMergeStreamingMessage, 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を取得できないタイプのオブジェクトだった
|
||||
// ストリームに来るのは通知かステータスだから、多分ここは通らない
|
||||
Column.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) {
|
||||
Column.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 -> {
|
||||
// スクロール位置が先頭なら先頭にする
|
||||
Column.log.d("mergeStreamingMessage: has VH. missing scroll position.")
|
||||
viewHolder?.scrollToTop()
|
||||
}
|
||||
|
||||
holder_sp.isHead -> {
|
||||
// スクロール位置が先頭なら先頭にする
|
||||
Column.log.d("mergeStreamingMessage: has VH. keep head. $holder_sp")
|
||||
holder.setScrollPosition(ScrollPosition())
|
||||
}
|
||||
|
||||
restore_idx < -1 -> {
|
||||
// 可視範囲の検出に失敗
|
||||
Column.log.d("mergeStreamingMessage: has VH. can't get visible range.")
|
||||
}
|
||||
|
||||
else -> {
|
||||
// 現在の要素が表示され続けるようにしたい
|
||||
Column.log.d("mergeStreamingMessage: has VH. added=$added")
|
||||
holder.setListItemTop(restore_idx + added, restore_y)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val scroll_save = this.scroll_save
|
||||
when {
|
||||
// スクロール位置が先頭なら先頭のまま
|
||||
scroll_save == null || scroll_save.isHead -> {
|
||||
}
|
||||
|
||||
// 現在の要素が表示され続けるようにしたい
|
||||
else -> scroll_save.adapterIndex += added
|
||||
}
|
||||
}
|
||||
|
||||
updateMisskeyCapture()
|
||||
}
|
||||
|
||||
private const val DIR_BACKGROUND_IMAGE = "columnBackground"
|
||||
|
||||
fun getBackgroundImageDir(context: Context): File {
|
||||
val externalDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
|
||||
if (externalDir == null) {
|
||||
Column.log.e("getExternalFilesDir is null.")
|
||||
} else {
|
||||
val state = Environment.getExternalStorageState()
|
||||
if (state != Environment.MEDIA_MOUNTED) {
|
||||
Column.log.e("getExternalStorageState: ${state}")
|
||||
} else {
|
||||
Column.log.i("externalDir: ${externalDir}")
|
||||
externalDir.mkdir()
|
||||
val backgroundDir = File(externalDir, DIR_BACKGROUND_IMAGE)
|
||||
backgroundDir.mkdir()
|
||||
Column.log.i("backgroundDir: ${backgroundDir} exists=${backgroundDir.exists()}")
|
||||
return backgroundDir
|
||||
}
|
||||
}
|
||||
val backgroundDir = context.getDir(DIR_BACKGROUND_IMAGE, Context.MODE_PRIVATE)
|
||||
Column.log.i("backgroundDir: ${backgroundDir} exists=${backgroundDir.exists()}")
|
||||
return backgroundDir
|
||||
}
|
|
@ -0,0 +1,416 @@
|
|||
package jp.juggler.subwaytooter
|
||||
|
||||
import android.content.Context
|
||||
import jp.juggler.subwaytooter.api.*
|
||||
import jp.juggler.subwaytooter.api.entity.*
|
||||
import jp.juggler.subwaytooter.table.FavMute
|
||||
import jp.juggler.subwaytooter.table.HighlightWord
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.subwaytooter.table.UserRelation
|
||||
import jp.juggler.util.WordTrieTree
|
||||
import java.util.regex.Pattern
|
||||
|
||||
// マストドン2.4.3rcのキーワードフィルタのコンテキスト
|
||||
fun Column.getFilterContext() = when (type) {
|
||||
|
||||
ColumnType.HOME, ColumnType.LIST_TL, ColumnType.MISSKEY_HYBRID -> TootFilter.CONTEXT_HOME
|
||||
|
||||
ColumnType.NOTIFICATIONS, ColumnType.NOTIFICATION_FROM_ACCT -> TootFilter.CONTEXT_NOTIFICATIONS
|
||||
|
||||
ColumnType.CONVERSATION -> TootFilter.CONTEXT_THREAD
|
||||
|
||||
ColumnType.DIRECT_MESSAGES -> TootFilter.CONTEXT_THREAD
|
||||
|
||||
ColumnType.PROFILE -> TootFilter.CONTEXT_PROFILE
|
||||
|
||||
else -> TootFilter.CONTEXT_PUBLIC
|
||||
// ColumnType.MISSKEY_HYBRID や ColumnType.MISSKEY_ANTENNA_TL はHOMEでもPUBLICでもある…
|
||||
// Misskeyだし関係ないが、NONEにするとアプリ内で完結するフィルタも働かなくなる
|
||||
}
|
||||
|
||||
|
||||
// カラム設定に正規表現フィルタを含めるなら真
|
||||
fun Column.canStatusFilter(): Boolean {
|
||||
if (getFilterContext() != TootFilter.CONTEXT_NONE) return true
|
||||
|
||||
return when (type) {
|
||||
ColumnType.SEARCH_MSP, ColumnType.SEARCH_TS, ColumnType.SEARCH_NOTESTOCK -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
// カラム設定に「すべての画像を隠す」ボタンを含めるなら真
|
||||
fun Column.canNSFWDefault(): Boolean = canStatusFilter()
|
||||
|
||||
// カラム設定に「ブーストを表示しない」ボタンを含めるなら真
|
||||
fun Column.canFilterBoost(): Boolean = when (type) {
|
||||
ColumnType.HOME, ColumnType.MISSKEY_HYBRID, ColumnType.PROFILE,
|
||||
ColumnType.NOTIFICATIONS, ColumnType.NOTIFICATION_FROM_ACCT,
|
||||
ColumnType.LIST_TL, ColumnType.MISSKEY_ANTENNA_TL -> true
|
||||
ColumnType.LOCAL, ColumnType.FEDERATE, ColumnType.HASHTAG, ColumnType.SEARCH -> isMisskey
|
||||
ColumnType.HASHTAG_FROM_ACCT -> false
|
||||
ColumnType.CONVERSATION, ColumnType.DIRECT_MESSAGES -> isMisskey
|
||||
else -> false
|
||||
}
|
||||
|
||||
// カラム設定に「返信を表示しない」ボタンを含めるなら真
|
||||
fun Column.canFilterReply(): Boolean = when (type) {
|
||||
ColumnType.HOME, ColumnType.MISSKEY_HYBRID, ColumnType.PROFILE,
|
||||
ColumnType.NOTIFICATIONS, ColumnType.NOTIFICATION_FROM_ACCT,
|
||||
ColumnType.LIST_TL, ColumnType.MISSKEY_ANTENNA_TL, ColumnType.DIRECT_MESSAGES -> true
|
||||
ColumnType.LOCAL, ColumnType.FEDERATE, ColumnType.HASHTAG, ColumnType.SEARCH -> isMisskey
|
||||
ColumnType.HASHTAG_FROM_ACCT -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
fun Column.canFilterNormalToot(): Boolean = when (type) {
|
||||
ColumnType.NOTIFICATIONS -> true
|
||||
ColumnType.HOME, ColumnType.MISSKEY_HYBRID,
|
||||
ColumnType.LIST_TL, ColumnType.MISSKEY_ANTENNA_TL -> true
|
||||
ColumnType.LOCAL, ColumnType.FEDERATE, ColumnType.HASHTAG, ColumnType.SEARCH -> isMisskey
|
||||
ColumnType.HASHTAG_FROM_ACCT -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
fun Column.canFilterNonPublicToot(): Boolean = when (type) {
|
||||
ColumnType.HOME, ColumnType.MISSKEY_HYBRID,
|
||||
ColumnType.LIST_TL, ColumnType.MISSKEY_ANTENNA_TL -> true
|
||||
ColumnType.LOCAL, ColumnType.FEDERATE, ColumnType.HASHTAG, ColumnType.SEARCH -> isMisskey
|
||||
ColumnType.HASHTAG_FROM_ACCT -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
|
||||
fun Column.onFiltersChanged2(filterList: ArrayList<TootFilter>) {
|
||||
val newFilter = encodeFilterTree(filterList) ?: return
|
||||
this.keywordFilterTrees = newFilter
|
||||
checkFiltersForListData(newFilter)
|
||||
}
|
||||
|
||||
fun Column.onFilterDeleted(filter: TootFilter, filterList: ArrayList<TootFilter>) {
|
||||
if (type == ColumnType.KEYWORD_FILTER) {
|
||||
val tmp_list = ArrayList<TimelineItem>(list_data.size)
|
||||
for (o in list_data) {
|
||||
if (o is TootFilter) {
|
||||
if (o.id == filter.id) continue
|
||||
}
|
||||
tmp_list.add(o)
|
||||
}
|
||||
if (tmp_list.size != list_data.size) {
|
||||
list_data.clear()
|
||||
list_data.addAll(tmp_list)
|
||||
fireShowContent(reason = "onFilterDeleted")
|
||||
}
|
||||
} else {
|
||||
val context = getFilterContext()
|
||||
if (context != TootFilter.CONTEXT_NONE) {
|
||||
onFiltersChanged2(filterList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Column.onLanguageFilterChanged() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
|
||||
fun Column.initFilter() {
|
||||
column_regex_filter = Column.COLUMN_REGEX_FILTER_DEFAULT
|
||||
val regex_text = this.regex_text
|
||||
if (regex_text.isNotEmpty()) {
|
||||
try {
|
||||
val re = Pattern.compile(regex_text)
|
||||
column_regex_filter =
|
||||
{ text: CharSequence? ->
|
||||
if (text?.isEmpty() != false)
|
||||
false
|
||||
else
|
||||
re.matcher(text).find()
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
Column.log.trace(ex)
|
||||
}
|
||||
}
|
||||
|
||||
favMuteSet = FavMute.acctSet
|
||||
highlight_trie = HighlightWord.nameSet
|
||||
}
|
||||
|
||||
private fun Column.isFilteredByAttachment(status: TootStatus): Boolean {
|
||||
// オプションがどれも設定されていないならフィルタしない(false)
|
||||
if (!(with_attachment || with_highlight)) return false
|
||||
|
||||
val matchMedia = with_attachment && status.reblog?.hasMedia() ?: status.hasMedia()
|
||||
val matchHighlight =
|
||||
with_highlight && null != (status.reblog?.highlightAny ?: status.highlightAny)
|
||||
|
||||
// どれかの条件を満たすならフィルタしない(false)、どれも満たさないならフィルタする(true)
|
||||
return !(matchMedia || matchHighlight)
|
||||
}
|
||||
|
||||
fun Column.isFiltered(status: TootStatus): Boolean {
|
||||
|
||||
val filterTrees = keywordFilterTrees
|
||||
if (filterTrees != null) {
|
||||
if (status.isKeywordFiltered(access_info, filterTrees.treeIrreversible)) {
|
||||
Column.log.d("status filtered by treeIrreversible")
|
||||
return true
|
||||
}
|
||||
|
||||
// just update _filtered flag for reversible filter
|
||||
status.updateKeywordFilteredFlag(access_info, filterTrees)
|
||||
}
|
||||
|
||||
if (isFilteredByAttachment(status)) return true
|
||||
|
||||
val reblog = status.reblog
|
||||
|
||||
if (dont_show_boost) {
|
||||
if (reblog != null) return true
|
||||
}
|
||||
|
||||
if (dont_show_reply) {
|
||||
if (status.in_reply_to_id != null) return true
|
||||
if (reblog?.in_reply_to_id != null) return true
|
||||
}
|
||||
|
||||
if (dont_show_normal_toot) {
|
||||
if (status.in_reply_to_id == null && reblog == null) return true
|
||||
}
|
||||
if (dont_show_non_public_toot) {
|
||||
if (!status.visibility.isPublic) return true
|
||||
}
|
||||
|
||||
if (column_regex_filter(status.decoded_content)) return true
|
||||
if (column_regex_filter(reblog?.decoded_content)) return true
|
||||
if (column_regex_filter(status.decoded_spoiler_text)) return true
|
||||
if (column_regex_filter(reblog?.decoded_spoiler_text)) return true
|
||||
|
||||
if (checkLanguageFilter(status)) return true
|
||||
|
||||
if (access_info.isPseudo) {
|
||||
var r = UserRelation.loadPseudo(access_info.getFullAcct(status.account))
|
||||
if (r.muting || r.blocking) return true
|
||||
if (reblog != null) {
|
||||
r = UserRelation.loadPseudo(access_info.getFullAcct(reblog.account))
|
||||
if (r.muting || r.blocking) return true
|
||||
}
|
||||
}
|
||||
|
||||
return status.checkMuted()
|
||||
}
|
||||
|
||||
// true if the status will be hidden
|
||||
private fun Column.checkLanguageFilter(status: TootStatus?): Boolean {
|
||||
status ?: return false
|
||||
val languageFilter = language_filter ?: return false
|
||||
|
||||
val allow = languageFilter.boolean(
|
||||
status.language ?: status.reblog?.language ?: TootStatus.LANGUAGE_CODE_UNKNOWN
|
||||
)
|
||||
?: languageFilter.boolean(TootStatus.LANGUAGE_CODE_DEFAULT)
|
||||
?: true
|
||||
|
||||
return !allow
|
||||
}
|
||||
|
||||
fun Column.isFiltered(item: TootNotification): Boolean {
|
||||
|
||||
if (when (quick_filter) {
|
||||
Column.QUICK_FILTER_ALL -> when (item.type) {
|
||||
TootNotification.TYPE_FAVOURITE -> dont_show_favourite
|
||||
|
||||
TootNotification.TYPE_REBLOG,
|
||||
TootNotification.TYPE_RENOTE,
|
||||
TootNotification.TYPE_QUOTE -> dont_show_boost
|
||||
|
||||
TootNotification.TYPE_FOLLOW,
|
||||
TootNotification.TYPE_UNFOLLOW,
|
||||
TootNotification.TYPE_FOLLOW_REQUEST,
|
||||
TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY,
|
||||
TootNotification.TYPE_FOLLOW_REQUEST_ACCEPTED_MISSKEY -> dont_show_follow
|
||||
|
||||
TootNotification.TYPE_MENTION,
|
||||
TootNotification.TYPE_REPLY -> dont_show_reply
|
||||
|
||||
TootNotification.TYPE_EMOJI_REACTION,
|
||||
TootNotification.TYPE_REACTION -> dont_show_reaction
|
||||
|
||||
TootNotification.TYPE_VOTE,
|
||||
TootNotification.TYPE_POLL,
|
||||
TootNotification.TYPE_POLL_VOTE_MISSKEY -> dont_show_vote
|
||||
|
||||
TootNotification.TYPE_STATUS -> dont_show_normal_toot
|
||||
else -> false
|
||||
}
|
||||
|
||||
else -> when (item.type) {
|
||||
TootNotification.TYPE_FAVOURITE -> quick_filter != Column.QUICK_FILTER_FAVOURITE
|
||||
TootNotification.TYPE_REBLOG,
|
||||
TootNotification.TYPE_RENOTE,
|
||||
TootNotification.TYPE_QUOTE -> quick_filter != Column.QUICK_FILTER_BOOST
|
||||
|
||||
TootNotification.TYPE_FOLLOW,
|
||||
TootNotification.TYPE_UNFOLLOW,
|
||||
TootNotification.TYPE_FOLLOW_REQUEST,
|
||||
TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY,
|
||||
TootNotification.TYPE_FOLLOW_REQUEST_ACCEPTED_MISSKEY -> quick_filter != Column.QUICK_FILTER_FOLLOW
|
||||
|
||||
TootNotification.TYPE_MENTION,
|
||||
TootNotification.TYPE_REPLY -> quick_filter != Column.QUICK_FILTER_MENTION
|
||||
|
||||
TootNotification.TYPE_EMOJI_REACTION,
|
||||
TootNotification.TYPE_REACTION -> quick_filter != Column.QUICK_FILTER_REACTION
|
||||
|
||||
TootNotification.TYPE_VOTE,
|
||||
TootNotification.TYPE_POLL,
|
||||
TootNotification.TYPE_POLL_VOTE_MISSKEY -> quick_filter != Column.QUICK_FILTER_VOTE
|
||||
|
||||
TootNotification.TYPE_STATUS -> quick_filter != Column.QUICK_FILTER_POST
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
) {
|
||||
Column.log.d("isFiltered: ${item.type} notification filtered.")
|
||||
return true
|
||||
}
|
||||
|
||||
val status = item.status
|
||||
val filterTrees = keywordFilterTrees
|
||||
if (status != null && filterTrees != null) {
|
||||
if (status.isKeywordFiltered(access_info, filterTrees.treeIrreversible)) {
|
||||
Column.log.d("isFiltered: status muted by treeIrreversible.")
|
||||
return true
|
||||
}
|
||||
|
||||
// just update _filtered flag for reversible filter
|
||||
status.updateKeywordFilteredFlag(access_info, filterTrees)
|
||||
}
|
||||
if (checkLanguageFilter(status)) return true
|
||||
|
||||
if (status?.checkMuted() == true) {
|
||||
Column.log.d("isFiltered: status muted by in-app muted words.")
|
||||
return true
|
||||
}
|
||||
|
||||
// ふぁぼ魔ミュート
|
||||
when (item.type) {
|
||||
TootNotification.TYPE_REBLOG,
|
||||
TootNotification.TYPE_RENOTE,
|
||||
TootNotification.TYPE_QUOTE,
|
||||
TootNotification.TYPE_FAVOURITE,
|
||||
TootNotification.TYPE_EMOJI_REACTION,
|
||||
TootNotification.TYPE_REACTION,
|
||||
TootNotification.TYPE_FOLLOW,
|
||||
TootNotification.TYPE_FOLLOW_REQUEST,
|
||||
TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY,
|
||||
TootNotification.TYPE_FOLLOW_REQUEST_ACCEPTED_MISSKEY -> {
|
||||
val who = item.account
|
||||
if (who != null && favMuteSet?.contains(access_info.getFullAcct(who)) == true) {
|
||||
Column.log.d("%s is in favMuteSet.", access_info.getFullAcct(who))
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// フィルタを読み直してリストを返す。またはnull
|
||||
suspend fun Column.loadFilter2(client: TootApiClient): ArrayList<TootFilter>? {
|
||||
if (access_info.isPseudo || access_info.isMisskey) return null
|
||||
val column_context = getFilterContext()
|
||||
if (column_context == 0) return null
|
||||
val result = client.request(ApiPath.PATH_FILTERS)
|
||||
|
||||
val jsonArray = result?.jsonArray ?: return null
|
||||
return TootFilter.parseList(jsonArray)
|
||||
}
|
||||
|
||||
fun Column.encodeFilterTree(filterList: ArrayList<TootFilter>?): FilterTrees? {
|
||||
val column_context = getFilterContext()
|
||||
if (column_context == 0 || filterList == null) return null
|
||||
val result = FilterTrees()
|
||||
val now = System.currentTimeMillis()
|
||||
for (filter in filterList) {
|
||||
if (filter.time_expires_at > 0L && now >= filter.time_expires_at) continue
|
||||
if ((filter.context and column_context) != 0) {
|
||||
|
||||
val validator = when (filter.whole_word) {
|
||||
true -> WordTrieTree.WORD_VALIDATOR
|
||||
else -> WordTrieTree.EMPTY_VALIDATOR
|
||||
}
|
||||
|
||||
if (filter.irreversible) {
|
||||
result.treeIrreversible
|
||||
} else {
|
||||
result.treeReversible
|
||||
}.add(filter.phrase, validator = validator)
|
||||
|
||||
result.treeAll.add(filter.phrase, validator = validator)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
fun Column.checkFiltersForListData(trees: FilterTrees?) {
|
||||
trees ?: return
|
||||
|
||||
val changeList = ArrayList<AdapterChange>()
|
||||
list_data.forEachIndexed { idx, item ->
|
||||
when (item) {
|
||||
is TootStatus -> {
|
||||
val old_filtered = item.filtered
|
||||
item.updateKeywordFilteredFlag(access_info, trees, checkIrreversible = true)
|
||||
if (old_filtered != item.filtered) {
|
||||
changeList.add(AdapterChange(AdapterChangeType.RangeChange, idx))
|
||||
}
|
||||
}
|
||||
|
||||
is TootNotification -> {
|
||||
val s = item.status
|
||||
if (s != null) {
|
||||
val old_filtered = s.filtered
|
||||
s.updateKeywordFilteredFlag(access_info, trees, checkIrreversible = true)
|
||||
if (old_filtered != s.filtered) {
|
||||
changeList.add(AdapterChange(AdapterChangeType.RangeChange, idx))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fireShowContent(reason = "filter updated", changeList = changeList)
|
||||
|
||||
}
|
||||
|
||||
|
||||
fun reloadFilter(context: Context, access_info: SavedAccount) {
|
||||
|
||||
TootTaskRunner(context, progress_style = TootTaskRunner.PROGRESS_NONE).run(access_info,
|
||||
object : TootTask {
|
||||
|
||||
var filter_list: java.util.ArrayList<TootFilter>? = null
|
||||
|
||||
override suspend fun background(client: TootApiClient): TootApiResult? {
|
||||
val result = client.request(ApiPath.PATH_FILTERS)
|
||||
val jsonArray = result?.jsonArray
|
||||
if (jsonArray != null) {
|
||||
filter_list = TootFilter.parseList(jsonArray)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override suspend fun handleResult(result: TootApiResult?) {
|
||||
val filter_list = this.filter_list
|
||||
if (filter_list != null) {
|
||||
Column.log.d("update filters for ${access_info.acct.pretty}")
|
||||
for (column in App1.getAppState(context).columnList) {
|
||||
if (column.access_info == access_info) {
|
||||
column.onFiltersChanged2(filter_list)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
|
@ -3,6 +3,7 @@ package jp.juggler.subwaytooter
|
|||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.SystemClock
|
||||
import jp.juggler.subwaytooter.api.ApiPath
|
||||
import jp.juggler.subwaytooter.api.TootApiClient
|
||||
import jp.juggler.subwaytooter.api.TootApiResult
|
||||
import jp.juggler.subwaytooter.api.TootParser
|
||||
|
@ -73,7 +74,7 @@ abstract class ColumnTask(
|
|||
else -> "active"
|
||||
}
|
||||
val local = ! column.search_resolve
|
||||
return "${Column.PATH_PROFILE_DIRECTORY}&order=$order&local=$local"
|
||||
return "${ApiPath.PATH_PROFILE_DIRECTORY}&order=$order&local=$local"
|
||||
}
|
||||
|
||||
internal suspend fun getAnnouncements(
|
||||
|
|
|
@ -943,7 +943,7 @@ class ColumnTask_Loading(
|
|||
}
|
||||
|
||||
suspend fun getScheduledStatuses(client: TootApiClient): TootApiResult? {
|
||||
val result = client.request(Column.PATH_SCHEDULED_STATUSES)
|
||||
val result = client.request(ApiPath.PATH_SCHEDULED_STATUSES)
|
||||
val src = parseList(::TootScheduled, parser, result?.jsonArray)
|
||||
list_tmp = addAll(list_tmp, src)
|
||||
|
||||
|
@ -1013,7 +1013,7 @@ class ColumnTask_Loading(
|
|||
// ステータスIDに該当するトゥート
|
||||
// タンスをまたいだりすると存在しないかもしれないが、エラーは出さない
|
||||
var result: TootApiResult? =
|
||||
client.request(String.format(Locale.JAPAN, Column.PATH_STATUSES, column.status_id))
|
||||
client.request(String.format(Locale.JAPAN, ApiPath.PATH_STATUSES, column.status_id))
|
||||
val target_status = parser.status(result?.jsonObject)
|
||||
if (target_status != null) {
|
||||
list_tmp = addOne(list_tmp, target_status)
|
||||
|
@ -1056,7 +1056,7 @@ class ColumnTask_Loading(
|
|||
// ステータスIDに該当するトゥート
|
||||
// タンスをまたいだりすると存在しないかもしれない
|
||||
var result: TootApiResult? =
|
||||
client.request(String.format(Locale.JAPAN, Column.PATH_STATUSES, column.status_id))
|
||||
client.request(String.format(Locale.JAPAN, ApiPath.PATH_STATUSES, column.status_id))
|
||||
val target_status = parser.status(result?.jsonObject) ?: return result
|
||||
list_tmp = addOne(list_tmp, target_status)
|
||||
|
||||
|
@ -1173,7 +1173,7 @@ class ColumnTask_Loading(
|
|||
} else {
|
||||
// 指定された発言そのもの
|
||||
var result = client.request(
|
||||
String.format(Locale.JAPAN, Column.PATH_STATUSES, column.status_id)
|
||||
String.format(Locale.JAPAN, ApiPath.PATH_STATUSES, column.status_id)
|
||||
)
|
||||
var jsonObject = result?.jsonObject ?: return result
|
||||
val target_status = parser.status(jsonObject)
|
||||
|
@ -1183,7 +1183,7 @@ class ColumnTask_Loading(
|
|||
result = client.request(
|
||||
String.format(
|
||||
Locale.JAPAN,
|
||||
Column.PATH_STATUSES_CONTEXT, column.status_id
|
||||
ApiPath.PATH_STATUSES_CONTEXT, column.status_id
|
||||
)
|
||||
)
|
||||
jsonObject = result?.jsonObject ?: return result
|
||||
|
|
|
@ -1206,7 +1206,7 @@ class ColumnTask_Refresh(
|
|||
}
|
||||
|
||||
suspend fun getScheduledStatuses(client: TootApiClient): TootApiResult? {
|
||||
val result = client.request(column.addRange(bBottom, Column.PATH_SCHEDULED_STATUSES))
|
||||
val result = client.request(column.addRange(bBottom, ApiPath.PATH_SCHEDULED_STATUSES))
|
||||
val src = parseList(::TootScheduled, parser, result?.jsonArray)
|
||||
list_tmp = addAll(list_tmp, src)
|
||||
column.saveRange(bBottom, !bBottom, result, src)
|
||||
|
|
|
@ -2,6 +2,7 @@ package jp.juggler.subwaytooter
|
|||
|
||||
import android.content.Context
|
||||
import android.view.Gravity
|
||||
import jp.juggler.subwaytooter.api.ApiPath
|
||||
import jp.juggler.subwaytooter.api.TootApiClient
|
||||
import jp.juggler.subwaytooter.api.TootApiResult
|
||||
import jp.juggler.subwaytooter.api.entity.*
|
||||
|
@ -134,21 +135,21 @@ enum class ColumnType(
|
|||
// 通常トゥートの取得
|
||||
getStatusList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_PROFILE_STATUSES,
|
||||
ApiPath.PATH_MISSKEY_PROFILE_STATUSES,
|
||||
misskeyParams = column.makeMisskeyParamsProfileStatuses(parser)
|
||||
)
|
||||
},
|
||||
refresh = { client ->
|
||||
getStatusList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_PROFILE_STATUSES,
|
||||
ApiPath.PATH_MISSKEY_PROFILE_STATUSES,
|
||||
misskeyParams = column.makeMisskeyParamsProfileStatuses(parser)
|
||||
)
|
||||
},
|
||||
gap = { client ->
|
||||
getStatusList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_PROFILE_STATUSES,
|
||||
ApiPath.PATH_MISSKEY_PROFILE_STATUSES,
|
||||
mastodonFilterByIdRange = true,
|
||||
misskeyParams = column.makeMisskeyParamsProfileStatuses(parser)
|
||||
)
|
||||
|
@ -161,20 +162,20 @@ enum class ColumnType(
|
|||
loading = { client ->
|
||||
getAccountList(
|
||||
client,
|
||||
String.format(Locale.JAPAN, Column.PATH_ACCOUNT_FOLLOWING, column.profile_id),
|
||||
String.format(Locale.JAPAN, ApiPath.PATH_ACCOUNT_FOLLOWING, column.profile_id),
|
||||
emptyMessage = context.getString(R.string.none_or_hidden_following)
|
||||
)
|
||||
},
|
||||
refresh = { client ->
|
||||
getAccountList(
|
||||
client,
|
||||
String.format(Locale.JAPAN, Column.PATH_ACCOUNT_FOLLOWING, column.profile_id)
|
||||
String.format(Locale.JAPAN, ApiPath.PATH_ACCOUNT_FOLLOWING, column.profile_id)
|
||||
)
|
||||
},
|
||||
gap = { client ->
|
||||
getAccountList(
|
||||
client,
|
||||
String.format(Locale.JAPAN, Column.PATH_ACCOUNT_FOLLOWING, column.profile_id),
|
||||
String.format(Locale.JAPAN, ApiPath.PATH_ACCOUNT_FOLLOWING, column.profile_id),
|
||||
mastodonFilterByIdRange = false,
|
||||
)
|
||||
},
|
||||
|
@ -200,7 +201,7 @@ enum class ColumnType(
|
|||
column.pagingType = ColumnPagingType.Cursor
|
||||
getAccountList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_PROFILE_FOLLOWING,
|
||||
ApiPath.PATH_MISSKEY_PROFILE_FOLLOWING,
|
||||
emptyMessage = context.getString(R.string.none_or_hidden_following),
|
||||
misskeyParams = column.makeMisskeyParamsUserId(parser),
|
||||
arrayFinder = misskeyArrayFinderUsers
|
||||
|
@ -209,7 +210,7 @@ enum class ColumnType(
|
|||
refresh = { client ->
|
||||
getAccountList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_PROFILE_FOLLOWING,
|
||||
ApiPath.PATH_MISSKEY_PROFILE_FOLLOWING,
|
||||
misskeyParams = column.makeMisskeyParamsUserId(parser),
|
||||
arrayFinder = misskeyArrayFinderUsers
|
||||
)
|
||||
|
@ -217,7 +218,7 @@ enum class ColumnType(
|
|||
gap = { client ->
|
||||
getAccountList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_PROFILE_FOLLOWING,
|
||||
ApiPath.PATH_MISSKEY_PROFILE_FOLLOWING,
|
||||
mastodonFilterByIdRange = false,
|
||||
misskeyParams = column.makeMisskeyParamsUserId(parser),
|
||||
arrayFinder = misskeyArrayFinderUsers
|
||||
|
@ -232,7 +233,7 @@ enum class ColumnType(
|
|||
column.pagingType = ColumnPagingType.Default
|
||||
getAccountList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_PROFILE_FOLLOWING,
|
||||
ApiPath.PATH_MISSKEY_PROFILE_FOLLOWING,
|
||||
emptyMessage = context.getString(R.string.none_or_hidden_following),
|
||||
misskeyParams = column.makeMisskeyParamsUserId(parser),
|
||||
listParser = misskey11FollowingParser
|
||||
|
@ -241,7 +242,7 @@ enum class ColumnType(
|
|||
refresh = { client ->
|
||||
getAccountList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_PROFILE_FOLLOWING,
|
||||
ApiPath.PATH_MISSKEY_PROFILE_FOLLOWING,
|
||||
misskeyParams = column.makeMisskeyParamsUserId(parser),
|
||||
listParser = misskey11FollowingParser
|
||||
)
|
||||
|
@ -249,7 +250,7 @@ enum class ColumnType(
|
|||
gap = { client ->
|
||||
getAccountList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_PROFILE_FOLLOWING,
|
||||
ApiPath.PATH_MISSKEY_PROFILE_FOLLOWING,
|
||||
mastodonFilterByIdRange = false,
|
||||
misskeyParams = column.makeMisskeyParamsUserId(parser),
|
||||
listParser = misskey11FollowingParser
|
||||
|
@ -264,7 +265,7 @@ enum class ColumnType(
|
|||
column.pagingType = ColumnPagingType.Default
|
||||
getAccountList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_PROFILE_FOLLOWERS,
|
||||
ApiPath.PATH_MISSKEY_PROFILE_FOLLOWERS,
|
||||
emptyMessage = context.getString(R.string.none_or_hidden_followers),
|
||||
misskeyParams = column.makeMisskeyParamsUserId(parser),
|
||||
listParser = misskey11FollowersParser
|
||||
|
@ -274,7 +275,7 @@ enum class ColumnType(
|
|||
refresh = { client ->
|
||||
getAccountList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_PROFILE_FOLLOWERS,
|
||||
ApiPath.PATH_MISSKEY_PROFILE_FOLLOWERS,
|
||||
misskeyParams = column.makeMisskeyParamsUserId(parser),
|
||||
listParser = misskey11FollowersParser
|
||||
)
|
||||
|
@ -282,7 +283,7 @@ enum class ColumnType(
|
|||
gap = { client ->
|
||||
getAccountList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_PROFILE_FOLLOWING,
|
||||
ApiPath.PATH_MISSKEY_PROFILE_FOLLOWING,
|
||||
mastodonFilterByIdRange = false,
|
||||
misskeyParams = column.makeMisskeyParamsUserId(parser),
|
||||
listParser = misskey11FollowersParser
|
||||
|
@ -297,7 +298,7 @@ enum class ColumnType(
|
|||
column.pagingType = ColumnPagingType.Cursor
|
||||
getAccountList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_PROFILE_FOLLOWERS,
|
||||
ApiPath.PATH_MISSKEY_PROFILE_FOLLOWERS,
|
||||
emptyMessage = context.getString(R.string.none_or_hidden_followers),
|
||||
misskeyParams = column.makeMisskeyParamsUserId(parser),
|
||||
arrayFinder = misskeyArrayFinderUsers
|
||||
|
@ -307,7 +308,7 @@ enum class ColumnType(
|
|||
refresh = { client ->
|
||||
getAccountList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_PROFILE_FOLLOWERS,
|
||||
ApiPath.PATH_MISSKEY_PROFILE_FOLLOWERS,
|
||||
misskeyParams = column.makeMisskeyParamsUserId(parser),
|
||||
arrayFinder = misskeyArrayFinderUsers
|
||||
)
|
||||
|
@ -315,7 +316,7 @@ enum class ColumnType(
|
|||
gap = { client ->
|
||||
getAccountList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_PROFILE_FOLLOWERS,
|
||||
ApiPath.PATH_MISSKEY_PROFILE_FOLLOWERS,
|
||||
mastodonFilterByIdRange = false,
|
||||
misskeyParams = column.makeMisskeyParamsUserId(parser)
|
||||
)
|
||||
|
@ -341,7 +342,7 @@ enum class ColumnType(
|
|||
loading = { client ->
|
||||
getAccountList(
|
||||
client,
|
||||
String.format(Locale.JAPAN, Column.PATH_ACCOUNT_FOLLOWERS, column.profile_id),
|
||||
String.format(Locale.JAPAN, ApiPath.PATH_ACCOUNT_FOLLOWERS, column.profile_id),
|
||||
emptyMessage = context.getString(R.string.none_or_hidden_followers)
|
||||
)
|
||||
},
|
||||
|
@ -349,13 +350,13 @@ enum class ColumnType(
|
|||
refresh = { client ->
|
||||
getAccountList(
|
||||
client,
|
||||
String.format(Locale.JAPAN, Column.PATH_ACCOUNT_FOLLOWERS, column.profile_id)
|
||||
String.format(Locale.JAPAN, ApiPath.PATH_ACCOUNT_FOLLOWERS, column.profile_id)
|
||||
)
|
||||
},
|
||||
gap = { client ->
|
||||
getAccountList(
|
||||
client,
|
||||
String.format(Locale.JAPAN, Column.PATH_ACCOUNT_FOLLOWERS, column.profile_id),
|
||||
String.format(Locale.JAPAN, ApiPath.PATH_ACCOUNT_FOLLOWERS, column.profile_id),
|
||||
mastodonFilterByIdRange = false
|
||||
)
|
||||
},
|
||||
|
@ -645,12 +646,12 @@ enum class ColumnType(
|
|||
if (isMisskey) {
|
||||
getStatusList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_FAVORITES,
|
||||
ApiPath.PATH_MISSKEY_FAVORITES,
|
||||
misskeyParams = column.makeMisskeyTimelineParameter(parser),
|
||||
listParser = misskeyCustomParserFavorites
|
||||
)
|
||||
} else {
|
||||
getStatusList(client, Column.PATH_FAVOURITES)
|
||||
getStatusList(client, ApiPath.PATH_FAVOURITES)
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -658,12 +659,12 @@ enum class ColumnType(
|
|||
if (isMisskey) {
|
||||
getStatusList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_FAVORITES,
|
||||
ApiPath.PATH_MISSKEY_FAVORITES,
|
||||
misskeyParams = column.makeMisskeyTimelineParameter(parser),
|
||||
listParser = misskeyCustomParserFavorites
|
||||
)
|
||||
} else {
|
||||
getStatusList(client, Column.PATH_FAVOURITES)
|
||||
getStatusList(client, ApiPath.PATH_FAVOURITES)
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -671,7 +672,7 @@ enum class ColumnType(
|
|||
if (isMisskey) {
|
||||
getStatusList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_FAVORITES,
|
||||
ApiPath.PATH_MISSKEY_FAVORITES,
|
||||
mastodonFilterByIdRange = false,
|
||||
misskeyParams = column.makeMisskeyTimelineParameter(parser),
|
||||
listParser = misskeyCustomParserFavorites
|
||||
|
@ -679,7 +680,7 @@ enum class ColumnType(
|
|||
} else {
|
||||
getStatusList(
|
||||
client,
|
||||
Column.PATH_FAVOURITES,
|
||||
ApiPath.PATH_FAVOURITES,
|
||||
mastodonFilterByIdRange = false
|
||||
)
|
||||
}
|
||||
|
@ -697,7 +698,7 @@ enum class ColumnType(
|
|||
if (isMisskey) {
|
||||
TootApiResult("Misskey has no bookmarks feature.")
|
||||
} else {
|
||||
getStatusList(client, Column.PATH_BOOKMARKS)
|
||||
getStatusList(client, ApiPath.PATH_BOOKMARKS)
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -705,7 +706,7 @@ enum class ColumnType(
|
|||
if (isMisskey) {
|
||||
TootApiResult("Misskey has no bookmarks feature.")
|
||||
} else {
|
||||
getStatusList(client, Column.PATH_BOOKMARKS)
|
||||
getStatusList(client, ApiPath.PATH_BOOKMARKS)
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -713,7 +714,7 @@ enum class ColumnType(
|
|||
if (isMisskey) {
|
||||
TootApiResult("Misskey has no bookmarks feature.")
|
||||
} else {
|
||||
getStatusList(client, Column.PATH_BOOKMARKS, mastodonFilterByIdRange = false)
|
||||
getStatusList(client, ApiPath.PATH_BOOKMARKS, mastodonFilterByIdRange = false)
|
||||
}
|
||||
},
|
||||
gapDirection = gapDirectionMastodonWorkaround,
|
||||
|
@ -941,14 +942,14 @@ enum class ColumnType(
|
|||
column.pagingType = ColumnPagingType.Default
|
||||
getAccountList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_MUTES,
|
||||
ApiPath.PATH_MISSKEY_MUTES,
|
||||
misskeyParams = access_info.putMisskeyApiToken(),
|
||||
listParser = misskeyCustomParserMutes
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
else -> getAccountList(client, Column.PATH_MUTES)
|
||||
else -> getAccountList(client, ApiPath.PATH_MUTES)
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -956,12 +957,12 @@ enum class ColumnType(
|
|||
when {
|
||||
isMisskey -> getAccountList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_MUTES,
|
||||
ApiPath.PATH_MISSKEY_MUTES,
|
||||
misskeyParams = access_info.putMisskeyApiToken(),
|
||||
arrayFinder = misskeyArrayFinderUsers,
|
||||
listParser = misskeyCustomParserMutes
|
||||
)
|
||||
else -> getAccountList(client, Column.PATH_MUTES)
|
||||
else -> getAccountList(client, ApiPath.PATH_MUTES)
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -969,7 +970,7 @@ enum class ColumnType(
|
|||
when {
|
||||
isMisskey -> getAccountList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_MUTES,
|
||||
ApiPath.PATH_MISSKEY_MUTES,
|
||||
mastodonFilterByIdRange = false,
|
||||
misskeyParams = access_info.putMisskeyApiToken(),
|
||||
arrayFinder = misskeyArrayFinderUsers,
|
||||
|
@ -977,7 +978,7 @@ enum class ColumnType(
|
|||
)
|
||||
else -> getAccountList(
|
||||
client,
|
||||
Column.PATH_MUTES,
|
||||
ApiPath.PATH_MUTES,
|
||||
mastodonFilterByIdRange = false
|
||||
)
|
||||
}
|
||||
|
@ -997,13 +998,13 @@ enum class ColumnType(
|
|||
column.pagingType = ColumnPagingType.Default
|
||||
getAccountList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_BLOCKS,
|
||||
ApiPath.PATH_MISSKEY_BLOCKS,
|
||||
misskeyParams = access_info.putMisskeyApiToken(),
|
||||
listParser = misskeyCustomParserBlocks
|
||||
)
|
||||
}
|
||||
|
||||
else -> getAccountList(client, Column.PATH_BLOCKS)
|
||||
else -> getAccountList(client, ApiPath.PATH_BLOCKS)
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -1012,13 +1013,13 @@ enum class ColumnType(
|
|||
isMisskey -> {
|
||||
getAccountList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_BLOCKS,
|
||||
ApiPath.PATH_MISSKEY_BLOCKS,
|
||||
misskeyParams = access_info.putMisskeyApiToken(),
|
||||
listParser = misskeyCustomParserBlocks
|
||||
)
|
||||
}
|
||||
|
||||
else -> getAccountList(client, Column.PATH_BLOCKS)
|
||||
else -> getAccountList(client, ApiPath.PATH_BLOCKS)
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -1027,14 +1028,14 @@ enum class ColumnType(
|
|||
isMisskey -> {
|
||||
getAccountList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_BLOCKS,
|
||||
ApiPath.PATH_MISSKEY_BLOCKS,
|
||||
mastodonFilterByIdRange = false,
|
||||
misskeyParams = access_info.putMisskeyApiToken(),
|
||||
listParser = misskeyCustomParserBlocks
|
||||
)
|
||||
}
|
||||
|
||||
else -> getAccountList(client, Column.PATH_BLOCKS, mastodonFilterByIdRange = false)
|
||||
else -> getAccountList(client, ApiPath.PATH_BLOCKS, mastodonFilterByIdRange = false)
|
||||
}
|
||||
},
|
||||
gapDirection = gapDirectionMastodonWorkaround,
|
||||
|
@ -1051,31 +1052,31 @@ enum class ColumnType(
|
|||
column.pagingType = ColumnPagingType.None
|
||||
getAccountList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_FOLLOW_REQUESTS,
|
||||
ApiPath.PATH_MISSKEY_FOLLOW_REQUESTS,
|
||||
misskeyParams = access_info.putMisskeyApiToken(),
|
||||
listParser = misskeyCustomParserFollowRequest
|
||||
)
|
||||
} else {
|
||||
getAccountList(client, Column.PATH_FOLLOW_REQUESTS)
|
||||
getAccountList(client, ApiPath.PATH_FOLLOW_REQUESTS)
|
||||
}
|
||||
},
|
||||
refresh = { client ->
|
||||
if (isMisskey) {
|
||||
getAccountList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_FOLLOW_REQUESTS,
|
||||
ApiPath.PATH_MISSKEY_FOLLOW_REQUESTS,
|
||||
misskeyParams = access_info.putMisskeyApiToken(),
|
||||
listParser = misskeyCustomParserFollowRequest
|
||||
)
|
||||
} else {
|
||||
getAccountList(client, Column.PATH_FOLLOW_REQUESTS)
|
||||
getAccountList(client, ApiPath.PATH_FOLLOW_REQUESTS)
|
||||
}
|
||||
},
|
||||
gap = { client ->
|
||||
if (isMisskey) {
|
||||
getAccountList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_FOLLOW_REQUESTS,
|
||||
ApiPath.PATH_MISSKEY_FOLLOW_REQUESTS,
|
||||
mastodonFilterByIdRange = false,
|
||||
misskeyParams = access_info.putMisskeyApiToken(),
|
||||
listParser = misskeyCustomParserFollowRequest
|
||||
|
@ -1083,7 +1084,7 @@ enum class ColumnType(
|
|||
} else {
|
||||
getAccountList(
|
||||
client,
|
||||
Column.PATH_FOLLOW_REQUESTS,
|
||||
ApiPath.PATH_FOLLOW_REQUESTS,
|
||||
mastodonFilterByIdRange = false
|
||||
)
|
||||
}
|
||||
|
@ -1099,19 +1100,19 @@ enum class ColumnType(
|
|||
loading = { client ->
|
||||
getAccountList(
|
||||
client,
|
||||
String.format(Locale.JAPAN, Column.PATH_BOOSTED_BY, column.status_id)
|
||||
String.format(Locale.JAPAN, ApiPath.PATH_BOOSTED_BY, column.status_id)
|
||||
)
|
||||
},
|
||||
refresh = { client ->
|
||||
getAccountList(
|
||||
client,
|
||||
String.format(Locale.JAPAN, Column.PATH_BOOSTED_BY, posted_status_id)
|
||||
String.format(Locale.JAPAN, ApiPath.PATH_BOOSTED_BY, posted_status_id)
|
||||
)
|
||||
},
|
||||
gap = { client ->
|
||||
getAccountList(
|
||||
client,
|
||||
String.format(Locale.JAPAN, Column.PATH_BOOSTED_BY, column.status_id),
|
||||
String.format(Locale.JAPAN, ApiPath.PATH_BOOSTED_BY, column.status_id),
|
||||
mastodonFilterByIdRange = false,
|
||||
)
|
||||
},
|
||||
|
@ -1126,19 +1127,19 @@ enum class ColumnType(
|
|||
loading = { client ->
|
||||
getAccountList(
|
||||
client,
|
||||
String.format(Locale.JAPAN, Column.PATH_FAVOURITED_BY, column.status_id)
|
||||
String.format(Locale.JAPAN, ApiPath.PATH_FAVOURITED_BY, column.status_id)
|
||||
)
|
||||
},
|
||||
refresh = { client ->
|
||||
getAccountList(
|
||||
client,
|
||||
String.format(Locale.JAPAN, Column.PATH_FAVOURITED_BY, posted_status_id)
|
||||
String.format(Locale.JAPAN, ApiPath.PATH_FAVOURITED_BY, posted_status_id)
|
||||
)
|
||||
},
|
||||
gap = { client ->
|
||||
getAccountList(
|
||||
client,
|
||||
String.format(Locale.JAPAN, Column.PATH_FAVOURITED_BY, column.status_id),
|
||||
String.format(Locale.JAPAN, ApiPath.PATH_FAVOURITED_BY, column.status_id),
|
||||
mastodonFilterByIdRange = false,
|
||||
)
|
||||
},
|
||||
|
@ -1151,8 +1152,8 @@ enum class ColumnType(
|
|||
bAllowPseudo = false,
|
||||
bAllowMisskey = false,
|
||||
|
||||
loading = { client -> getDomainBlockList(client, Column.PATH_DOMAIN_BLOCK) },
|
||||
refresh = { client -> getDomainList(client, Column.PATH_DOMAIN_BLOCK) }
|
||||
loading = { client -> getDomainBlockList(client, ApiPath.PATH_DOMAIN_BLOCK) },
|
||||
refresh = { client -> getDomainList(client, ApiPath.PATH_DOMAIN_BLOCK) }
|
||||
),
|
||||
|
||||
SEARCH_MSP(
|
||||
|
@ -1242,7 +1243,7 @@ enum class ColumnType(
|
|||
misskeyParams = column.makeMisskeyBaseParameter(parser)
|
||||
)
|
||||
} else {
|
||||
getListList(client, Column.PATH_LIST_LIST)
|
||||
getListList(client, ApiPath.PATH_LIST_LIST)
|
||||
}
|
||||
}
|
||||
),
|
||||
|
@ -1349,7 +1350,7 @@ enum class ColumnType(
|
|||
} else {
|
||||
getAccountList(
|
||||
client,
|
||||
String.format(Locale.JAPAN, Column.PATH_LIST_MEMBER, column.profile_id)
|
||||
String.format(Locale.JAPAN, ApiPath.PATH_LIST_MEMBER, column.profile_id)
|
||||
)
|
||||
}
|
||||
},
|
||||
|
@ -1358,7 +1359,7 @@ enum class ColumnType(
|
|||
column.loadListInfo(client, false)
|
||||
getAccountList(
|
||||
client,
|
||||
String.format(Locale.JAPAN, Column.PATH_LIST_MEMBER, column.profile_id)
|
||||
String.format(Locale.JAPAN, ApiPath.PATH_LIST_MEMBER, column.profile_id)
|
||||
)
|
||||
}
|
||||
),
|
||||
|
@ -1371,10 +1372,10 @@ enum class ColumnType(
|
|||
loading = { client ->
|
||||
column.useConversationSummaries = false
|
||||
if (column.use_old_api) {
|
||||
getStatusList(client, Column.PATH_DIRECT_MESSAGES)
|
||||
getStatusList(client, ApiPath.PATH_DIRECT_MESSAGES)
|
||||
} else {
|
||||
// try 2.6.0 new API https://github.com/tootsuite/mastodon/pull/8832
|
||||
val result = getConversationSummary(client, Column.PATH_DIRECT_MESSAGES2)
|
||||
val result = getConversationSummary(client, ApiPath.PATH_DIRECT_MESSAGES2)
|
||||
when {
|
||||
// cancelled
|
||||
result == null -> null
|
||||
|
@ -1386,7 +1387,7 @@ enum class ColumnType(
|
|||
}
|
||||
|
||||
// fallback to old api
|
||||
else -> getStatusList(client, Column.PATH_DIRECT_MESSAGES)
|
||||
else -> getStatusList(client, ApiPath.PATH_DIRECT_MESSAGES)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1394,10 +1395,10 @@ enum class ColumnType(
|
|||
refresh = { client ->
|
||||
if (column.useConversationSummaries) {
|
||||
// try 2.6.0 new API https://github.com/tootsuite/mastodon/pull/8832
|
||||
getConversationSummaryList(client, Column.PATH_DIRECT_MESSAGES2)
|
||||
getConversationSummaryList(client, ApiPath.PATH_DIRECT_MESSAGES2)
|
||||
} else {
|
||||
// fallback to old api
|
||||
getStatusList(client, Column.PATH_DIRECT_MESSAGES)
|
||||
getStatusList(client, ApiPath.PATH_DIRECT_MESSAGES)
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -1406,12 +1407,12 @@ enum class ColumnType(
|
|||
// try 2.6.0 new API https://github.com/tootsuite/mastodon/pull/8832
|
||||
getConversationSummaryList(
|
||||
client,
|
||||
Column.PATH_DIRECT_MESSAGES2,
|
||||
ApiPath.PATH_DIRECT_MESSAGES2,
|
||||
mastodonFilterByIdRange = false
|
||||
)
|
||||
} else {
|
||||
// fallback to old api
|
||||
getStatusList(client, Column.PATH_DIRECT_MESSAGES, mastodonFilterByIdRange = false)
|
||||
getStatusList(client, ApiPath.PATH_DIRECT_MESSAGES, mastodonFilterByIdRange = false)
|
||||
}
|
||||
},
|
||||
gapDirection = gapDirectionMastodonWorkaround,
|
||||
|
@ -1466,7 +1467,7 @@ enum class ColumnType(
|
|||
column.pagingType = ColumnPagingType.Offset
|
||||
getAccountList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_FOLLOW_SUGGESTION,
|
||||
ApiPath.PATH_MISSKEY_FOLLOW_SUGGESTION,
|
||||
misskeyParams = access_info.putMisskeyApiToken()
|
||||
)
|
||||
} else {
|
||||
|
@ -1474,9 +1475,9 @@ enum class ColumnType(
|
|||
when {
|
||||
ti == null -> ri
|
||||
ti.versionGE(TootInstance.VERSION_3_4_0_rc1) ->
|
||||
getAccountList(client, Column.PATH_FOLLOW_SUGGESTION2)
|
||||
getAccountList(client, ApiPath.PATH_FOLLOW_SUGGESTION2)
|
||||
else ->
|
||||
getAccountList(client, Column.PATH_FOLLOW_SUGGESTION)
|
||||
getAccountList(client, ApiPath.PATH_FOLLOW_SUGGESTION)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1485,7 +1486,7 @@ enum class ColumnType(
|
|||
if (isMisskey) {
|
||||
getAccountList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_FOLLOW_SUGGESTION,
|
||||
ApiPath.PATH_MISSKEY_FOLLOW_SUGGESTION,
|
||||
misskeyParams = access_info.putMisskeyApiToken()
|
||||
)
|
||||
} else {
|
||||
|
@ -1493,9 +1494,9 @@ enum class ColumnType(
|
|||
when {
|
||||
ti == null -> ri
|
||||
ti.versionGE(TootInstance.VERSION_3_4_0_rc1) ->
|
||||
getAccountList(client, Column.PATH_FOLLOW_SUGGESTION2)
|
||||
getAccountList(client, ApiPath.PATH_FOLLOW_SUGGESTION2)
|
||||
else ->
|
||||
getAccountList(client, Column.PATH_FOLLOW_SUGGESTION)
|
||||
getAccountList(client, ApiPath.PATH_FOLLOW_SUGGESTION)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1504,7 +1505,7 @@ enum class ColumnType(
|
|||
if (isMisskey) {
|
||||
getAccountList(
|
||||
client,
|
||||
Column.PATH_MISSKEY_FOLLOW_SUGGESTION,
|
||||
ApiPath.PATH_MISSKEY_FOLLOW_SUGGESTION,
|
||||
mastodonFilterByIdRange = false,
|
||||
misskeyParams = access_info.putMisskeyApiToken()
|
||||
)
|
||||
|
@ -1515,13 +1516,13 @@ enum class ColumnType(
|
|||
ti.versionGE(TootInstance.VERSION_3_4_0_rc1) ->
|
||||
getAccountList(
|
||||
client,
|
||||
Column.PATH_FOLLOW_SUGGESTION2,
|
||||
ApiPath.PATH_FOLLOW_SUGGESTION2,
|
||||
mastodonFilterByIdRange = false
|
||||
)
|
||||
else ->
|
||||
getAccountList(
|
||||
client,
|
||||
Column.PATH_FOLLOW_SUGGESTION,
|
||||
ApiPath.PATH_FOLLOW_SUGGESTION,
|
||||
mastodonFilterByIdRange = false
|
||||
)
|
||||
}
|
||||
|
@ -1537,12 +1538,12 @@ enum class ColumnType(
|
|||
bAllowPseudo = false,
|
||||
bAllowMisskey = false,
|
||||
|
||||
loading = { client -> getAccountList(client, Column.PATH_ENDORSEMENT) },
|
||||
refresh = { client -> getAccountList(client, Column.PATH_ENDORSEMENT) },
|
||||
loading = { client -> getAccountList(client, ApiPath.PATH_ENDORSEMENT) },
|
||||
refresh = { client -> getAccountList(client, ApiPath.PATH_ENDORSEMENT) },
|
||||
gap = { client ->
|
||||
getAccountList(
|
||||
client,
|
||||
Column.PATH_ENDORSEMENT,
|
||||
ApiPath.PATH_ENDORSEMENT,
|
||||
mastodonFilterByIdRange = false
|
||||
)
|
||||
},
|
||||
|
@ -1585,12 +1586,12 @@ enum class ColumnType(
|
|||
iconId = { R.drawable.ic_info },
|
||||
name1 = { it.getString(R.string.reports) },
|
||||
|
||||
loading = { client -> getReportList(client, Column.PATH_REPORTS) },
|
||||
refresh = { client -> getReportList(client, Column.PATH_REPORTS) },
|
||||
loading = { client -> getReportList(client, ApiPath.PATH_REPORTS) },
|
||||
refresh = { client -> getReportList(client, ApiPath.PATH_REPORTS) },
|
||||
gap = { client ->
|
||||
getReportList(
|
||||
client,
|
||||
Column.PATH_REPORTS,
|
||||
ApiPath.PATH_REPORTS,
|
||||
mastodonFilterByIdRange = false
|
||||
)
|
||||
},
|
||||
|
@ -1604,7 +1605,7 @@ enum class ColumnType(
|
|||
bAllowMisskey = false,
|
||||
headerType = HeaderType.Filter,
|
||||
|
||||
loading = { client -> getFilterList(client, Column.PATH_FILTERS) }
|
||||
loading = { client -> getFilterList(client, ApiPath.PATH_FILTERS) }
|
||||
),
|
||||
|
||||
SCHEDULED_STATUS(33,
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
package jp.juggler.subwaytooter
|
||||
|
||||
import android.util.LruCache
|
||||
import jp.juggler.subwaytooter.Column.Companion.READ_LIMIT
|
||||
import jp.juggler.subwaytooter.api.ApiPath.READ_LIMIT
|
||||
import jp.juggler.subwaytooter.Column.Companion.log
|
||||
import jp.juggler.subwaytooter.api.TootApiClient
|
||||
import jp.juggler.subwaytooter.api.TootApiResult
|
||||
import jp.juggler.subwaytooter.api.TootParser
|
||||
import jp.juggler.subwaytooter.api.*
|
||||
import jp.juggler.subwaytooter.api.entity.*
|
||||
import jp.juggler.subwaytooter.api.syncAccountByAcct
|
||||
import jp.juggler.util.*
|
||||
import java.util.*
|
||||
|
||||
|
@ -406,7 +403,7 @@ internal suspend fun Column.makeNotificationUrl(
|
|||
access_info.isMisskey -> "/api/i/notifications"
|
||||
|
||||
else -> {
|
||||
val sb = StringBuilder(Column.PATH_NOTIFICATIONS) // always contain "?limit=XX"
|
||||
val sb = StringBuilder(ApiPath.PATH_NOTIFICATIONS) // always contain "?limit=XX"
|
||||
when (val quick_filter = quick_filter) {
|
||||
Column.QUICK_FILTER_ALL -> {
|
||||
if (dont_show_favourite) sb.append("&exclude_types[]=favourite")
|
||||
|
|
|
@ -0,0 +1,183 @@
|
|||
package jp.juggler.subwaytooter
|
||||
|
||||
import jp.juggler.subwaytooter.api.TootApiClient
|
||||
import jp.juggler.subwaytooter.api.TootParser
|
||||
import jp.juggler.subwaytooter.api.entity.*
|
||||
import jp.juggler.subwaytooter.table.AcctSet
|
||||
import jp.juggler.subwaytooter.table.TagSet
|
||||
import jp.juggler.subwaytooter.table.UserRelation
|
||||
import jp.juggler.util.toJsonArray
|
||||
import jp.juggler.util.toPostRequestBuilder
|
||||
import java.util.HashSet
|
||||
|
||||
class UpdateRelationEnv(val column: Column) {
|
||||
|
||||
val who_set = HashSet<EntityId>()
|
||||
val acct_set = HashSet<String>()
|
||||
val tag_set = HashSet<String>()
|
||||
|
||||
fun add(whoRef: TootAccountRef?) {
|
||||
add(whoRef?.get())
|
||||
}
|
||||
|
||||
fun add(who: TootAccount?) {
|
||||
who ?: return
|
||||
who_set.add(who.id)
|
||||
val fullAcct = column.access_info.getFullAcct(who)
|
||||
acct_set.add("@${fullAcct.ascii}")
|
||||
acct_set.add("@${fullAcct.pretty}")
|
||||
//
|
||||
add(who.movedRef)
|
||||
}
|
||||
|
||||
fun add(s: TootStatus?) {
|
||||
if (s == null) return
|
||||
add(s.accountRef)
|
||||
add(s.reblog)
|
||||
s.tags?.forEach { tag_set.add(it.name) }
|
||||
}
|
||||
|
||||
fun add(n: TootNotification?) {
|
||||
if (n == null) return
|
||||
add(n.accountRef)
|
||||
add(n.status)
|
||||
}
|
||||
|
||||
suspend fun update(client: TootApiClient, parser: TootParser) {
|
||||
|
||||
var n: Int
|
||||
var size: Int
|
||||
|
||||
if (column.isMisskey) {
|
||||
|
||||
// parser内部にアカウントIDとRelationのマップが生成されるので、それをデータベースに記録する
|
||||
run {
|
||||
val now = System.currentTimeMillis()
|
||||
val who_list =
|
||||
parser.misskeyUserRelationMap.entries.toMutableList()
|
||||
var start = 0
|
||||
val end = who_list.size
|
||||
while (start < end) {
|
||||
var step = end - start
|
||||
if (step > Column.RELATIONSHIP_LOAD_STEP) step = Column.RELATIONSHIP_LOAD_STEP
|
||||
UserRelation.saveListMisskey(now, column.access_info.db_id, who_list, start, step)
|
||||
start += step
|
||||
}
|
||||
Column.log.d("updateRelation: update %d relations.", end)
|
||||
}
|
||||
|
||||
// 2018/11/1 Misskeyにもリレーション取得APIができた
|
||||
// アカウントIDの集合からRelationshipを取得してデータベースに記録する
|
||||
|
||||
size = who_set.size
|
||||
if (size > 0) {
|
||||
val who_list = ArrayList<EntityId>(size)
|
||||
who_list.addAll(who_set)
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
n = 0
|
||||
while (n < who_list.size) {
|
||||
val userIdList = ArrayList<EntityId>(Column.RELATIONSHIP_LOAD_STEP)
|
||||
for (i in 0 until Column.RELATIONSHIP_LOAD_STEP) {
|
||||
if (n >= size) break
|
||||
if (!parser.misskeyUserRelationMap.containsKey(who_list[n])) {
|
||||
userIdList.add(who_list[n])
|
||||
}
|
||||
++n
|
||||
}
|
||||
if (userIdList.isEmpty()) continue
|
||||
|
||||
val result = client.request(
|
||||
"/api/users/relation",
|
||||
column.access_info.putMisskeyApiToken().apply {
|
||||
put(
|
||||
"userId",
|
||||
userIdList.map { it.toString() }.toJsonArray()
|
||||
)
|
||||
}.toPostRequestBuilder()
|
||||
)
|
||||
|
||||
if (result == null || result.response?.code in 400 until 500) break
|
||||
|
||||
val list = parseList(::TootRelationShip, parser, result.jsonArray)
|
||||
if (list.size == userIdList.size) {
|
||||
for (i in 0 until list.size) {
|
||||
list[i].id = userIdList[i]
|
||||
}
|
||||
UserRelation.saveList2(now, column.access_info.db_id, list)
|
||||
}
|
||||
}
|
||||
Column.log.d("updateRelation: update %d relations.", n)
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
// アカウントIDの集合からRelationshipを取得してデータベースに記録する
|
||||
size = who_set.size
|
||||
if (size > 0) {
|
||||
val who_list = ArrayList<EntityId>(size)
|
||||
who_list.addAll(who_set)
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
n = 0
|
||||
while (n < who_list.size) {
|
||||
val sb = StringBuilder()
|
||||
sb.append("/api/v1/accounts/relationships")
|
||||
for (i in 0 until Column.RELATIONSHIP_LOAD_STEP) {
|
||||
if (n >= size) break
|
||||
sb.append(if (i == 0) '?' else '&')
|
||||
sb.append("id[]=")
|
||||
sb.append(who_list[n++].toString())
|
||||
}
|
||||
val result = client.request(sb.toString()) ?: break // cancelled.
|
||||
val list = parseList(::TootRelationShip, parser, result.jsonArray)
|
||||
if (list.size > 0) UserRelation.saveListMastodon(
|
||||
now,
|
||||
column.access_info.db_id,
|
||||
list
|
||||
)
|
||||
}
|
||||
Column.log.d("updateRelation: update %d relations.", n)
|
||||
}
|
||||
}
|
||||
|
||||
// 出現したacctをデータベースに記録する
|
||||
size = acct_set.size
|
||||
if (size > 0) {
|
||||
val acct_list = ArrayList<String?>(size)
|
||||
acct_list.addAll(acct_set)
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
n = 0
|
||||
while (n < acct_list.size) {
|
||||
var length = size - n
|
||||
if (length > Column.ACCT_DB_STEP) length = Column.ACCT_DB_STEP
|
||||
AcctSet.saveList(now, acct_list, n, length)
|
||||
n += length
|
||||
}
|
||||
Column.log.d("updateRelation: update %d acct.", n)
|
||||
|
||||
}
|
||||
|
||||
// 出現したタグをデータベースに記録する
|
||||
size = tag_set.size
|
||||
if (size > 0) {
|
||||
val tag_list = ArrayList<String?>(size)
|
||||
tag_list.addAll(tag_set)
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
|
||||
n = 0
|
||||
while (n < tag_list.size) {
|
||||
var length = size - n
|
||||
if (length > Column.ACCT_DB_STEP) length = Column.ACCT_DB_STEP
|
||||
TagSet.saveList(now, tag_list, n, length)
|
||||
n += length
|
||||
}
|
||||
Column.log.d("updateRelation: update %d tag.", n)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,14 +1,11 @@
|
|||
package jp.juggler.subwaytooter.action
|
||||
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import jp.juggler.subwaytooter.ActMain
|
||||
import jp.juggler.subwaytooter.ColumnType
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.*
|
||||
import jp.juggler.subwaytooter.api.*
|
||||
import jp.juggler.subwaytooter.api.entity.*
|
||||
import jp.juggler.subwaytooter.dialog.AccountPicker
|
||||
import jp.juggler.subwaytooter.dialog.DlgConfirm
|
||||
import jp.juggler.subwaytooter.removeUser
|
||||
import jp.juggler.subwaytooter.table.AcctColor
|
||||
import jp.juggler.subwaytooter.table.SavedAccount
|
||||
import jp.juggler.subwaytooter.table.UserRelation
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
package jp.juggler.subwaytooter.api
|
||||
|
||||
object ApiPath {
|
||||
const val READ_LIMIT = 80 // API側の上限が80です。ただし指定しても40しか返ってこないことが多い
|
||||
// ステータスのリストを返すAPI
|
||||
const val PATH_DIRECT_MESSAGES = "/api/v1/timelines/direct?limit=$READ_LIMIT"
|
||||
const val PATH_DIRECT_MESSAGES2 = "/api/v1/conversations?limit=$READ_LIMIT"
|
||||
|
||||
const val PATH_FAVOURITES = "/api/v1/favourites?limit=$READ_LIMIT"
|
||||
const val PATH_BOOKMARKS = "/api/v1/bookmarks?limit=$READ_LIMIT"
|
||||
|
||||
// アカウントのリストを返すAPI
|
||||
const val PATH_ACCOUNT_FOLLOWING =
|
||||
"/api/v1/accounts/%s/following?limit=$READ_LIMIT" // 1:account_id
|
||||
const val PATH_ACCOUNT_FOLLOWERS =
|
||||
"/api/v1/accounts/%s/followers?limit=$READ_LIMIT" // 1:account_id
|
||||
const val PATH_MUTES = "/api/v1/mutes?limit=$READ_LIMIT"
|
||||
const val PATH_BLOCKS = "/api/v1/blocks?limit=$READ_LIMIT"
|
||||
const val PATH_FOLLOW_REQUESTS = "/api/v1/follow_requests?limit=$READ_LIMIT"
|
||||
const val PATH_FOLLOW_SUGGESTION = "/api/v1/suggestions?limit=$READ_LIMIT"
|
||||
const val PATH_FOLLOW_SUGGESTION2 = "/api/v2/suggestions?limit=$READ_LIMIT"
|
||||
const val PATH_ENDORSEMENT = "/api/v1/endorsements?limit=$READ_LIMIT"
|
||||
|
||||
const val PATH_PROFILE_DIRECTORY = "/api/v1/directory?limit=$READ_LIMIT"
|
||||
|
||||
const val PATH_BOOSTED_BY =
|
||||
"/api/v1/statuses/%s/reblogged_by?limit=$READ_LIMIT" // 1:status_id
|
||||
const val PATH_FAVOURITED_BY =
|
||||
"/api/v1/statuses/%s/favourited_by?limit=$READ_LIMIT" // 1:status_id
|
||||
const val PATH_LIST_MEMBER = "/api/v1/lists/%s/accounts?limit=$READ_LIMIT"
|
||||
|
||||
// 他のリストを返すAPI
|
||||
const val PATH_REPORTS = "/api/v1/reports?limit=$READ_LIMIT"
|
||||
const val PATH_NOTIFICATIONS = "/api/v1/notifications?limit=$READ_LIMIT"
|
||||
const val PATH_DOMAIN_BLOCK = "/api/v1/domain_blocks?limit=$READ_LIMIT"
|
||||
const val PATH_LIST_LIST = "/api/v1/lists?limit=$READ_LIMIT"
|
||||
const val PATH_SCHEDULED_STATUSES = "/api/v1/scheduled_statuses?limit=$READ_LIMIT"
|
||||
|
||||
// リストではなくオブジェクトを返すAPI
|
||||
const val PATH_STATUSES = "/api/v1/statuses/%s" // 1:status_id
|
||||
const val PATH_STATUSES_CONTEXT = "/api/v1/statuses/%s/context" // 1:status_id
|
||||
// search args 1: query(urlencoded) , also, append "&resolve=1" if resolve non-local accounts
|
||||
|
||||
const val PATH_FILTERS = "/api/v1/filters"
|
||||
|
||||
const val PATH_MISSKEY_PROFILE_FOLLOWING = "/api/users/following"
|
||||
const val PATH_MISSKEY_PROFILE_FOLLOWERS = "/api/users/followers"
|
||||
const val PATH_MISSKEY_PROFILE_STATUSES = "/api/users/notes"
|
||||
|
||||
const val PATH_MISSKEY_MUTES = "/api/mute/list"
|
||||
const val PATH_MISSKEY_BLOCKS = "/api/blocking/list"
|
||||
const val PATH_MISSKEY_FOLLOW_REQUESTS = "/api/following/requests/list"
|
||||
const val PATH_MISSKEY_FOLLOW_SUGGESTION = "/api/users/recommendation"
|
||||
const val PATH_MISSKEY_FAVORITES = "/api/i/favorites"
|
||||
}
|
|
@ -8,6 +8,7 @@ import jp.juggler.subwaytooter.api.TootApiClient
|
|||
import jp.juggler.subwaytooter.api.TootApiResult
|
||||
import jp.juggler.subwaytooter.api.entity.*
|
||||
import jp.juggler.subwaytooter.onStatusRemoved
|
||||
import jp.juggler.subwaytooter.reloadFilter
|
||||
import jp.juggler.util.*
|
||||
import okhttp3.Response
|
||||
import okhttp3.WebSocket
|
||||
|
@ -221,7 +222,7 @@ class StreamConnection(
|
|||
log.d("$name handleMastodonMessage: missing event parameter")
|
||||
|
||||
"filters_changed" ->
|
||||
Column.onFiltersChanged(manager.context, acctGroup.account)
|
||||
reloadFilter(manager.context, acctGroup.account)
|
||||
|
||||
else -> {
|
||||
val payload = TootPayload.parsePayload(acctGroup.parser, event, obj, text)
|
||||
|
|
|
@ -4,7 +4,7 @@ import android.content.ContentValues
|
|||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.provider.BaseColumns
|
||||
import jp.juggler.subwaytooter.App1
|
||||
import jp.juggler.subwaytooter.Column
|
||||
import jp.juggler.subwaytooter.api.ApiPath
|
||||
import jp.juggler.subwaytooter.api.TootApiClient
|
||||
import jp.juggler.subwaytooter.api.TootApiResult
|
||||
import jp.juggler.subwaytooter.api.entity.EntityId
|
||||
|
@ -115,7 +115,7 @@ class NotificationCache(private val account_db_id: Long) {
|
|||
accessInfo.isMisskey -> "/api/i/notifications"
|
||||
|
||||
else -> {
|
||||
val sb = StringBuilder(Column.PATH_NOTIFICATIONS) // always contain "?limit=XX"
|
||||
val sb = StringBuilder(ApiPath.PATH_NOTIFICATIONS) // always contain "?limit=XX"
|
||||
|
||||
if (since_id != null) sb.append("&since_id=$since_id")
|
||||
|
||||
|
|
Loading…
Reference in New Issue