refactor column class

This commit is contained in:
tateisu 2021-05-17 23:13:04 +09:00
parent a2d91673e6
commit d831215ac4
20 changed files with 1724 additions and 1664 deletions

View File

@ -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)

View 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

View File

@ -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()
)
}

View File

@ -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 ->

View File

@ -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

View File

@ -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 -> {
}
}
}
}
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}
}
}
}
})
}

View File

@ -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(

View File

@ -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

View File

@ -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)

View File

@ -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,

View File

@ -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")

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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"
}

View File

@ -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)

View File

@ -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")