2018-01-04 19:52:25 +01:00
|
|
|
|
package jp.juggler.subwaytooter
|
|
|
|
|
|
|
|
|
|
import android.annotation.SuppressLint
|
|
|
|
|
import android.content.Context
|
2018-11-19 23:46:14 +01:00
|
|
|
|
import android.content.SharedPreferences
|
2018-09-07 13:34:54 +02:00
|
|
|
|
import android.os.Environment
|
2018-01-04 19:52:25 +01:00
|
|
|
|
import android.os.SystemClock
|
2019-08-23 01:49:20 +02:00
|
|
|
|
import android.util.SparseArray
|
2018-11-18 15:29:35 +01:00
|
|
|
|
import android.view.View
|
2019-04-28 13:30:47 +02:00
|
|
|
|
import androidx.appcompat.app.AppCompatActivity
|
2018-01-04 19:52:25 +01:00
|
|
|
|
import jp.juggler.subwaytooter.api.*
|
|
|
|
|
import jp.juggler.subwaytooter.api.entity.*
|
2020-12-11 18:30:10 +01:00
|
|
|
|
import jp.juggler.subwaytooter.notification.PollingWorker
|
2020-12-21 03:13:03 +01:00
|
|
|
|
import jp.juggler.subwaytooter.streaming.*
|
2018-03-15 17:23:43 +01:00
|
|
|
|
import jp.juggler.subwaytooter.table.*
|
2018-12-01 00:02:18 +01:00
|
|
|
|
import jp.juggler.subwaytooter.util.BucketList
|
|
|
|
|
import jp.juggler.subwaytooter.util.ScrollPosition
|
2020-09-08 01:55:15 +02:00
|
|
|
|
import jp.juggler.subwaytooter.util.matchHost
|
2018-12-01 00:02:18 +01:00
|
|
|
|
import jp.juggler.util.*
|
2018-12-07 01:50:11 +01:00
|
|
|
|
import okhttp3.Handshake
|
2019-01-20 22:04:57 +01:00
|
|
|
|
import org.jetbrains.anko.backgroundDrawable
|
2018-09-07 13:34:54 +02:00
|
|
|
|
import java.io.File
|
2018-08-16 21:58:30 +02:00
|
|
|
|
import java.lang.ref.WeakReference
|
2018-09-07 07:51:59 +02:00
|
|
|
|
import java.nio.ByteBuffer
|
2018-06-11 02:33:01 +02:00
|
|
|
|
import java.util.*
|
2018-08-25 13:59:57 +02:00
|
|
|
|
import java.util.concurrent.ConcurrentLinkedQueue
|
2018-08-16 21:58:30 +02:00
|
|
|
|
import java.util.concurrent.atomic.AtomicBoolean
|
2019-04-28 13:30:47 +02:00
|
|
|
|
import java.util.concurrent.atomic.AtomicInteger
|
2018-08-25 13:59:57 +02:00
|
|
|
|
import java.util.concurrent.atomic.AtomicLong
|
2018-08-16 21:58:30 +02:00
|
|
|
|
import java.util.regex.Pattern
|
2018-08-28 18:24:36 +02:00
|
|
|
|
import kotlin.collections.ArrayList
|
2020-12-22 05:27:39 +01:00
|
|
|
|
import kotlin.math.max
|
2018-01-04 19:52:25 +01:00
|
|
|
|
|
2018-04-20 15:22:21 +02:00
|
|
|
|
|
2019-06-05 23:06:37 +02:00
|
|
|
|
enum class ColumnPagingType {
|
2020-12-06 21:19:02 +01:00
|
|
|
|
Default,
|
|
|
|
|
Cursor,
|
|
|
|
|
Offset,
|
|
|
|
|
None,
|
2018-04-21 01:16:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
2020-12-06 21:19:02 +01:00
|
|
|
|
enum class ProfileTab(val id: Int, val ct: ColumnType) {
|
|
|
|
|
Status(0, ColumnType.TabStatus),
|
|
|
|
|
Following(1, ColumnType.TabFollowing),
|
|
|
|
|
Followers(2, ColumnType.TabFollowers)
|
2019-08-23 01:49:20 +02:00
|
|
|
|
}
|
|
|
|
|
|
2020-12-06 21:19:02 +01:00
|
|
|
|
enum class HeaderType(val viewType: Int) {
|
|
|
|
|
Profile(1),
|
|
|
|
|
Search(2),
|
|
|
|
|
Instance(3),
|
|
|
|
|
Filter(4),
|
|
|
|
|
ProfileDirectory(5),
|
2019-08-29 18:16:21 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
|
class Column(
|
2020-12-21 03:13:03 +01:00
|
|
|
|
val app_state: AppState,
|
|
|
|
|
val context: Context,
|
|
|
|
|
val access_info: SavedAccount,
|
|
|
|
|
typeId: Int,
|
|
|
|
|
val column_id: String
|
2018-01-12 10:01:25 +01:00
|
|
|
|
) {
|
2020-12-06 21:19:02 +01:00
|
|
|
|
companion object {
|
|
|
|
|
|
|
|
|
|
internal val log = LogCategory("Column")
|
|
|
|
|
|
|
|
|
|
private const val DIR_BACKGROUND_IMAGE = "columnBackground"
|
|
|
|
|
|
|
|
|
|
internal const val READ_LIMIT = 80 // API側の上限が80です。ただし指定しても40しか返ってこないことが多い
|
|
|
|
|
internal const val LOOP_TIMEOUT = 10000L
|
|
|
|
|
internal const val LOOP_READ_ENOUGH = 30 // フィルタ後のデータ数がコレ以上ならループを諦めます
|
|
|
|
|
internal const val RELATIONSHIP_LOAD_STEP = 40
|
|
|
|
|
internal const val ACCT_DB_STEP = 100
|
|
|
|
|
|
|
|
|
|
internal const val MISSKEY_HASHTAG_LIMIT = 30
|
|
|
|
|
|
|
|
|
|
// ステータスのリストを返すAPI
|
|
|
|
|
internal const val PATH_DIRECT_MESSAGES = "/api/v1/timelines/direct?limit=$READ_LIMIT"
|
|
|
|
|
internal const val PATH_DIRECT_MESSAGES2 = "/api/v1/conversations?limit=$READ_LIMIT"
|
|
|
|
|
|
|
|
|
|
internal const val PATH_FAVOURITES = "/api/v1/favourites?limit=$READ_LIMIT"
|
|
|
|
|
internal const val PATH_BOOKMARKS = "/api/v1/bookmarks?limit=$READ_LIMIT"
|
|
|
|
|
|
|
|
|
|
// アカウントのリストを返すAPI
|
|
|
|
|
internal const val PATH_ACCOUNT_FOLLOWING =
|
|
|
|
|
"/api/v1/accounts/%s/following?limit=$READ_LIMIT" // 1:account_id
|
|
|
|
|
internal const val PATH_ACCOUNT_FOLLOWERS =
|
|
|
|
|
"/api/v1/accounts/%s/followers?limit=$READ_LIMIT" // 1:account_id
|
|
|
|
|
internal const val PATH_MUTES = "/api/v1/mutes?limit=$READ_LIMIT"
|
|
|
|
|
internal const val PATH_BLOCKS = "/api/v1/blocks?limit=$READ_LIMIT"
|
|
|
|
|
internal const val PATH_FOLLOW_REQUESTS = "/api/v1/follow_requests?limit=$READ_LIMIT"
|
|
|
|
|
internal const val PATH_FOLLOW_SUGGESTION = "/api/v1/suggestions?limit=$READ_LIMIT"
|
2021-05-10 20:34:51 +02:00
|
|
|
|
internal const val PATH_FOLLOW_SUGGESTION2 = "/api/v2/suggestions?limit=$READ_LIMIT"
|
2020-12-06 21:19:02 +01:00
|
|
|
|
internal const val PATH_ENDORSEMENT = "/api/v1/endorsements?limit=$READ_LIMIT"
|
|
|
|
|
|
|
|
|
|
internal const val PATH_PROFILE_DIRECTORY = "/api/v1/directory?limit=$READ_LIMIT"
|
|
|
|
|
|
|
|
|
|
internal const val PATH_BOOSTED_BY =
|
|
|
|
|
"/api/v1/statuses/%s/reblogged_by?limit=$READ_LIMIT" // 1:status_id
|
|
|
|
|
internal const val PATH_FAVOURITED_BY =
|
|
|
|
|
"/api/v1/statuses/%s/favourited_by?limit=$READ_LIMIT" // 1:status_id
|
|
|
|
|
internal const val PATH_LIST_MEMBER = "/api/v1/lists/%s/accounts?limit=$READ_LIMIT"
|
|
|
|
|
|
|
|
|
|
// 他のリストを返すAPI
|
|
|
|
|
internal const val PATH_REPORTS = "/api/v1/reports?limit=$READ_LIMIT"
|
|
|
|
|
internal const val PATH_NOTIFICATIONS = "/api/v1/notifications?limit=$READ_LIMIT"
|
|
|
|
|
internal const val PATH_DOMAIN_BLOCK = "/api/v1/domain_blocks?limit=$READ_LIMIT"
|
|
|
|
|
internal const val PATH_LIST_LIST = "/api/v1/lists?limit=$READ_LIMIT"
|
|
|
|
|
internal const val PATH_SCHEDULED_STATUSES = "/api/v1/scheduled_statuses?limit=$READ_LIMIT"
|
|
|
|
|
|
|
|
|
|
// リストではなくオブジェクトを返すAPI
|
|
|
|
|
internal const val PATH_ACCOUNT = "/api/v1/accounts/%s" // 1:account_id
|
|
|
|
|
internal const val PATH_STATUSES = "/api/v1/statuses/%s" // 1:status_id
|
|
|
|
|
internal 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_PROFILE = "/api/users/show"
|
|
|
|
|
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"
|
|
|
|
|
|
|
|
|
|
internal const val KEY_ACCOUNT_ROW_ID = "account_id"
|
|
|
|
|
internal const val KEY_TYPE = "type"
|
|
|
|
|
internal const val KEY_COLUMN_ID = "column_id"
|
|
|
|
|
internal 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"
|
|
|
|
|
|
|
|
|
|
internal const val KEY_COLUMN_ACCESS_ACCT = "column_access"
|
|
|
|
|
internal const val KEY_COLUMN_ACCESS_STR = "column_access_str"
|
|
|
|
|
internal const val KEY_COLUMN_ACCESS_COLOR = "column_access_color"
|
|
|
|
|
internal const val KEY_COLUMN_ACCESS_COLOR_BG = "column_access_color_bg"
|
|
|
|
|
internal const val KEY_COLUMN_NAME = "column_name"
|
|
|
|
|
internal const val KEY_OLD_INDEX = "old_index"
|
|
|
|
|
|
|
|
|
|
internal const val KEY_ANNOUNCEMENT_HIDE_TIME = "announcementHideTime"
|
|
|
|
|
|
|
|
|
|
val typeMap: SparseArray<ColumnType> = SparseArray()
|
|
|
|
|
|
|
|
|
|
internal var showOpenSticker = false
|
|
|
|
|
|
|
|
|
|
internal const val QUICK_FILTER_ALL = 0
|
|
|
|
|
internal const val QUICK_FILTER_MENTION = 1
|
|
|
|
|
internal const val QUICK_FILTER_FAVOURITE = 2
|
|
|
|
|
internal const val QUICK_FILTER_BOOST = 3
|
|
|
|
|
internal const val QUICK_FILTER_FOLLOW = 4
|
|
|
|
|
internal const val QUICK_FILTER_REACTION = 5
|
|
|
|
|
internal const val QUICK_FILTER_VOTE = 6
|
|
|
|
|
internal const val QUICK_FILTER_POST = 7
|
|
|
|
|
|
|
|
|
|
internal const val HASHTAG_ELLIPSIZE = 26
|
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
|
2020-12-06 21:19:02 +01:00
|
|
|
|
@Suppress("UNCHECKED_CAST")
|
|
|
|
|
private inline fun <reified T> getParamAt(params: Array<out Any>, idx: Int): T {
|
|
|
|
|
return params[idx] as T
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun getParamEntityId(
|
2020-12-21 03:13:03 +01:00
|
|
|
|
params: Array<out Any>,
|
|
|
|
|
@Suppress("SameParameterValue") idx: Int
|
|
|
|
|
): EntityId =
|
2020-12-06 21:19:02 +01:00
|
|
|
|
when (val o = params[idx]) {
|
2020-12-21 03:13:03 +01:00
|
|
|
|
is EntityId -> o
|
|
|
|
|
is String -> EntityId(o)
|
2020-12-06 21:19:02 +01:00
|
|
|
|
else -> error("getParamEntityId [$idx] bad type. $o")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun getParamString(params: Array<out Any>, idx: Int): String =
|
|
|
|
|
when (val o = params[idx]) {
|
2020-12-21 03:13:03 +01:00
|
|
|
|
is String -> o
|
|
|
|
|
is EntityId -> o.toString()
|
|
|
|
|
is Host -> o.ascii
|
|
|
|
|
is Acct -> o.ascii
|
2020-12-06 21:19:02 +01:00
|
|
|
|
else -> error("getParamString [$idx] bad type. $o")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Suppress("UNCHECKED_CAST")
|
|
|
|
|
private inline fun <reified T> getParamAtNullable(params: Array<out Any>, idx: Int): T? {
|
|
|
|
|
if (idx >= params.size) return null
|
|
|
|
|
return params[idx] as T
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun loadAccount(context: Context, src: JsonObject): SavedAccount {
|
|
|
|
|
val account_db_id = src.long(KEY_ACCOUNT_ROW_ID) ?: -1L
|
|
|
|
|
return if (account_db_id >= 0) {
|
|
|
|
|
SavedAccount.loadAccount(context, account_db_id)
|
|
|
|
|
?: throw RuntimeException("missing account")
|
|
|
|
|
} else {
|
|
|
|
|
SavedAccount.na
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
// private val channelIdSeed = AtomicInteger(0)
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
|
|
|
|
// より古いデータの取得に使う
|
|
|
|
|
internal val reMaxId = """[&?]max_id=([^&?;\s]+)""".asciiPattern()
|
|
|
|
|
|
|
|
|
|
// より新しいデータの取得に使う
|
|
|
|
|
private val reMinId = """[&?](min_id|since_id)=([^&?;\s]+)""".asciiPattern()
|
|
|
|
|
|
|
|
|
|
val COLUMN_REGEX_FILTER_DEFAULT: (CharSequence?) -> Boolean = { false }
|
|
|
|
|
|
|
|
|
|
fun onFiltersChanged(context: Context, access_info: SavedAccount) {
|
|
|
|
|
|
|
|
|
|
TootTaskRunner(context, progress_style = TootTaskRunner.PROGRESS_NONE).run(access_info,
|
2020-12-21 03:13:03 +01:00
|
|
|
|
object : TootTask {
|
|
|
|
|
|
|
|
|
|
var filter_list: ArrayList<TootFilter>? = null
|
|
|
|
|
|
|
|
|
|
override suspend fun background(client: TootApiClient): TootApiResult? {
|
|
|
|
|
val result = client.request(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) {
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
2020-12-06 21:19:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private val columnIdMap = HashMap<String, WeakReference<Column>?>()
|
|
|
|
|
private fun registerColumnId(id: String, column: Column) {
|
|
|
|
|
synchronized(columnIdMap) {
|
|
|
|
|
columnIdMap[id] = WeakReference(column)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun decodeColumnId(src: JsonObject): String {
|
|
|
|
|
return src.string(KEY_COLUMN_ID) ?: generateColumnId()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun findColumnById(id: String): Column? {
|
|
|
|
|
synchronized(columnIdMap) {
|
|
|
|
|
return columnIdMap[id]?.get()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun getBackgroundImageDir(context: Context): File {
|
|
|
|
|
val externalDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
|
|
|
|
|
if (externalDir == null) {
|
|
|
|
|
log.e("getExternalFilesDir is null.")
|
|
|
|
|
} else {
|
|
|
|
|
val state = Environment.getExternalStorageState()
|
|
|
|
|
if (state != Environment.MEDIA_MOUNTED) {
|
|
|
|
|
log.e("getExternalStorageState: ${state}")
|
|
|
|
|
} else {
|
|
|
|
|
log.i("externalDir: ${externalDir}")
|
|
|
|
|
externalDir.mkdir()
|
|
|
|
|
val backgroundDir = File(externalDir, DIR_BACKGROUND_IMAGE)
|
|
|
|
|
backgroundDir.mkdir()
|
|
|
|
|
log.i("backgroundDir: ${backgroundDir} exists=${backgroundDir.exists()}")
|
|
|
|
|
return backgroundDir
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
val backgroundDir = context.getDir(DIR_BACKGROUND_IMAGE, Context.MODE_PRIVATE)
|
|
|
|
|
log.i("backgroundDir: ${backgroundDir} exists=${backgroundDir.exists()}")
|
|
|
|
|
return backgroundDir
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private var defaultColorHeaderBg = 0
|
|
|
|
|
private var defaultColorHeaderName = 0
|
|
|
|
|
private var defaultColorHeaderPageNumber = 0
|
|
|
|
|
var defaultColorContentBg = 0
|
|
|
|
|
private var defaultColorContentAcct = 0
|
|
|
|
|
private var defaultColorContentText = 0
|
|
|
|
|
|
|
|
|
|
fun reloadDefaultColor(activity: AppCompatActivity, pref: SharedPreferences) {
|
|
|
|
|
|
|
|
|
|
defaultColorHeaderBg = Pref.ipCcdHeaderBg(pref).notZero()
|
2021-01-04 02:11:45 +01:00
|
|
|
|
?: activity.attrColor(R.attr.color_column_header)
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
|
|
|
|
defaultColorHeaderName = Pref.ipCcdHeaderFg(pref).notZero()
|
2021-01-04 02:11:45 +01:00
|
|
|
|
?: activity.attrColor(R.attr.colorColumnHeaderName)
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
|
|
|
|
defaultColorHeaderPageNumber = Pref.ipCcdHeaderFg(pref).notZero()
|
2021-01-04 02:11:45 +01:00
|
|
|
|
?: activity.attrColor(R.attr.colorColumnHeaderPageNumber)
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
|
|
|
|
defaultColorContentBg = Pref.ipCcdContentBg(pref)
|
|
|
|
|
// may zero
|
|
|
|
|
|
|
|
|
|
defaultColorContentAcct = Pref.ipCcdContentAcct(pref).notZero()
|
2021-01-04 02:11:45 +01:00
|
|
|
|
?: activity.attrColor(R.attr.colorTimeSmall)
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
|
|
|
|
defaultColorContentText = Pref.ipCcdContentText(pref).notZero()
|
2021-01-04 02:11:45 +01:00
|
|
|
|
?: activity.attrColor(R.attr.colorContentText)
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
|
|
|
|
}
|
2020-12-21 03:13:03 +01:00
|
|
|
|
|
|
|
|
|
private val internalIdSeed = AtomicInteger(0)
|
2020-12-06 21:19:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
// カラムオブジェクトの識別に使うID。
|
|
|
|
|
val internalId = internalIdSeed.incrementAndGet()
|
|
|
|
|
|
2020-12-06 21:19:02 +01:00
|
|
|
|
val type = ColumnType.parse(typeId)
|
|
|
|
|
|
|
|
|
|
internal var dont_close: Boolean = false
|
|
|
|
|
|
|
|
|
|
internal var with_attachment: Boolean = false
|
|
|
|
|
internal var with_highlight: Boolean = false
|
|
|
|
|
internal var dont_show_boost: Boolean = false
|
|
|
|
|
internal var dont_show_reply: Boolean = false
|
|
|
|
|
|
|
|
|
|
internal var dont_show_normal_toot: Boolean = false
|
|
|
|
|
internal var dont_show_non_public_toot: Boolean = false
|
|
|
|
|
|
|
|
|
|
internal var dont_show_favourite: Boolean = false // 通知カラムのみ
|
|
|
|
|
internal var dont_show_follow: Boolean = false // 通知カラムのみ
|
|
|
|
|
internal var dont_show_reaction: Boolean = false // 通知カラムのみ
|
|
|
|
|
internal var dont_show_vote: Boolean = false // 通知カラムのみ
|
|
|
|
|
|
|
|
|
|
internal var quick_filter = QUICK_FILTER_ALL
|
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
@Volatile
|
2020-12-06 21:19:02 +01:00
|
|
|
|
internal var dont_streaming: Boolean = false
|
2020-12-21 03:13:03 +01:00
|
|
|
|
|
2020-12-06 21:19:02 +01:00
|
|
|
|
internal var dont_auto_refresh: Boolean = false
|
|
|
|
|
internal var hide_media_default: Boolean = false
|
|
|
|
|
internal var system_notification_not_related: Boolean = false
|
|
|
|
|
internal var instance_local: Boolean = false
|
|
|
|
|
|
|
|
|
|
internal var enable_speech: Boolean = false
|
|
|
|
|
internal var use_old_api = false
|
|
|
|
|
|
|
|
|
|
internal var regex_text: String = ""
|
|
|
|
|
|
|
|
|
|
internal var header_bg_color: Int = 0
|
|
|
|
|
internal var header_fg_color: Int = 0
|
|
|
|
|
internal var column_bg_color: Int = 0
|
|
|
|
|
internal var acct_color: Int = 0
|
|
|
|
|
internal var content_color: Int = 0
|
|
|
|
|
internal var column_bg_image: String = ""
|
|
|
|
|
internal var column_bg_image_alpha = 1f
|
|
|
|
|
|
|
|
|
|
internal var profile_tab = ProfileTab.Status
|
|
|
|
|
|
|
|
|
|
internal var status_id: EntityId? = null
|
|
|
|
|
|
|
|
|
|
// プロフカラムではアカウントのID。リストカラムではリストのID
|
|
|
|
|
internal var profile_id: EntityId? = null
|
|
|
|
|
|
|
|
|
|
internal var search_query: String = ""
|
|
|
|
|
internal var search_resolve: Boolean = false
|
|
|
|
|
internal var remote_only: Boolean = false
|
|
|
|
|
internal var instance_uri: String = ""
|
|
|
|
|
internal var hashtag: String = ""
|
|
|
|
|
internal var hashtag_any: String = ""
|
|
|
|
|
internal var hashtag_all: String = ""
|
|
|
|
|
internal var hashtag_none: String = ""
|
|
|
|
|
internal var hashtag_acct: String = ""
|
|
|
|
|
|
|
|
|
|
internal var language_filter: JsonObject? = null
|
|
|
|
|
|
|
|
|
|
// 告知のリスト
|
|
|
|
|
internal var announcements: MutableList<TootAnnouncement>? = null
|
|
|
|
|
|
|
|
|
|
// 表示中の告知
|
|
|
|
|
internal var announcementId: EntityId? = null
|
|
|
|
|
|
|
|
|
|
// 告知を閉じた時刻, 0なら閉じていない
|
|
|
|
|
internal var announcementHideTime = 0L
|
|
|
|
|
|
|
|
|
|
// 告知データを更新したタイミング
|
|
|
|
|
internal var announcementUpdated = 0L
|
|
|
|
|
|
|
|
|
|
// プロフカラムでのアカウント情報
|
|
|
|
|
@Volatile
|
|
|
|
|
internal var who_account: TootAccountRef? = null
|
|
|
|
|
|
|
|
|
|
// プロフカラムでのfeatured tag 情報(Mastodon3.3.0)
|
|
|
|
|
@Volatile
|
|
|
|
|
internal var who_featured_tags: List<TootTag>? = null
|
|
|
|
|
|
|
|
|
|
// リストカラムでのリスト情報
|
|
|
|
|
@Volatile
|
|
|
|
|
internal var list_info: TootList? = null
|
|
|
|
|
|
|
|
|
|
// アンテナカラムでのリスト情報
|
|
|
|
|
@Volatile
|
|
|
|
|
internal var antenna_info: MisskeyAntenna? = null
|
|
|
|
|
|
|
|
|
|
// 「インスタンス情報」カラムに表示するインスタンス情報
|
|
|
|
|
// (SavedAccount中のインスタンス情報とは異なるので注意)
|
|
|
|
|
internal var instance_information: TootInstance? = null
|
|
|
|
|
internal var handshake: Handshake? = null
|
|
|
|
|
|
|
|
|
|
internal var scroll_save: ScrollPosition? = null
|
|
|
|
|
private var last_viewing_item_id: EntityId? = null
|
|
|
|
|
|
|
|
|
|
internal val is_dispose = AtomicBoolean()
|
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
@Volatile
|
2020-12-06 21:19:02 +01:00
|
|
|
|
internal var bFirstInitialized = false
|
|
|
|
|
|
|
|
|
|
var filter_reload_required: Boolean = false
|
|
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
|
|
// カラムを閉じた後のnotifyDataSetChangedのタイミングで、add/removeされる順序が期待通りにならないので
|
|
|
|
|
// 参照を1つだけ持つのではなく、リストを保持して先頭の要素を使うことにする
|
|
|
|
|
|
|
|
|
|
private val _holder_list = LinkedList<ColumnViewHolder>()
|
|
|
|
|
|
|
|
|
|
internal // 複数のリスナがある場合、最も新しいものを返す
|
|
|
|
|
val viewHolder: ColumnViewHolder?
|
|
|
|
|
get() {
|
|
|
|
|
if (is_dispose.get()) return null
|
|
|
|
|
return if (_holder_list.isEmpty()) null else _holder_list.first
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
|
|
internal var lastTask: ColumnTask? = null
|
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
@Volatile
|
2020-12-06 21:19:02 +01:00
|
|
|
|
internal var bInitialLoading: Boolean = false
|
2020-12-21 03:13:03 +01:00
|
|
|
|
|
|
|
|
|
@Volatile
|
2020-12-06 21:19:02 +01:00
|
|
|
|
internal var bRefreshLoading: Boolean = false
|
|
|
|
|
|
|
|
|
|
internal var mInitialLoadingError: String = ""
|
|
|
|
|
internal var mRefreshLoadingError: String = ""
|
|
|
|
|
internal var mRefreshLoadingErrorTime: Long = 0L
|
|
|
|
|
internal var mRefreshLoadingErrorPopupState: Int = 0
|
|
|
|
|
|
|
|
|
|
internal var task_progress: String? = null
|
|
|
|
|
|
|
|
|
|
internal val list_data = BucketList<TimelineItem>()
|
|
|
|
|
internal val duplicate_map = DuplicateMap()
|
|
|
|
|
|
|
|
|
|
internal val isFilterEnabled: Boolean
|
|
|
|
|
get() = (with_attachment
|
|
|
|
|
|| with_highlight
|
|
|
|
|
|| regex_text.isNotEmpty()
|
|
|
|
|
|| dont_show_normal_toot
|
|
|
|
|
|| dont_show_non_public_toot
|
|
|
|
|
|| quick_filter != QUICK_FILTER_ALL
|
|
|
|
|
|| dont_show_boost
|
|
|
|
|
|| dont_show_favourite
|
|
|
|
|
|| dont_show_follow
|
|
|
|
|
|| dont_show_reply
|
|
|
|
|
|| dont_show_reaction
|
|
|
|
|
|| dont_show_vote
|
|
|
|
|
|| (language_filter?.isNotEmpty() == true)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@Volatile
|
|
|
|
|
private var column_regex_filter = COLUMN_REGEX_FILTER_DEFAULT
|
|
|
|
|
|
|
|
|
|
@Volatile
|
|
|
|
|
internal var keywordFilterTrees: FilterTrees? = null
|
|
|
|
|
|
|
|
|
|
@Volatile
|
|
|
|
|
private var favMuteSet: HashSet<Acct>? = null
|
|
|
|
|
|
|
|
|
|
@Volatile
|
|
|
|
|
internal var highlight_trie: WordTrieTree? = null
|
|
|
|
|
|
|
|
|
|
// タイムライン中のデータの始端と終端
|
|
|
|
|
// misskeyは
|
|
|
|
|
internal var idRecent: EntityId? = null
|
|
|
|
|
internal var idOld: EntityId? = null
|
|
|
|
|
internal var offsetNext: Int = 0
|
|
|
|
|
internal var pagingType: ColumnPagingType = ColumnPagingType.Default
|
|
|
|
|
|
|
|
|
|
var bRefreshingTop: Boolean = false
|
|
|
|
|
|
|
|
|
|
// ListViewの表示更新が追いつかないとスクロール位置が崩れるので
|
|
|
|
|
// 一定時間より短期間にはデータ更新しないようにする
|
|
|
|
|
private val last_show_stream_data = AtomicLong(0L)
|
|
|
|
|
private val stream_data_queue = ConcurrentLinkedQueue<TimelineItem>()
|
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
@Volatile
|
2020-12-06 21:19:02 +01:00
|
|
|
|
private var bPutGap: Boolean = false
|
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
private var cacheHeaderDesc: String? = null
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
// DMカラム更新時に新APIの利用に成功したなら真
|
|
|
|
|
internal var useConversationSummarys = false
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
// DMカラムのストリーミングイベントで新形式のイベントを利用できたなら真
|
|
|
|
|
internal var useConversationSummaryStreaming = false
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
////////////////////////////////////////////////////////////////
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-22 05:27:39 +01:00
|
|
|
|
private fun canHandleStreamingMessage() = !is_dispose.get() && canStartStreaming()
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-22 05:27:39 +01:00
|
|
|
|
private fun runOnMainLooperForStreamingEvent(proc: () -> Unit) {
|
2020-12-22 00:49:26 +01:00
|
|
|
|
runOnMainLooper {
|
2020-12-22 05:27:39 +01:00
|
|
|
|
if (!canHandleStreamingMessage())
|
2020-12-22 00:49:26 +01:00
|
|
|
|
return@runOnMainLooper
|
|
|
|
|
proc()
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
val streamCallback = object : StreamCallback {
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-27 08:10:58 +01:00
|
|
|
|
override fun onStreamStatusChanged(status: StreamStatus) {
|
2021-05-17 07:03:18 +02:00
|
|
|
|
log.d(
|
|
|
|
|
"onStreamStatusChanged status=${status}, bFirstInitialized=$bFirstInitialized, bInitialLoading=$bInitialLoading, column=${access_info.acct}/${
|
|
|
|
|
getColumnName(
|
|
|
|
|
true
|
|
|
|
|
)
|
|
|
|
|
}"
|
|
|
|
|
)
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-27 08:10:58 +01:00
|
|
|
|
if (status == StreamStatus.Subscribed) {
|
2020-12-22 00:49:26 +01:00
|
|
|
|
updateMisskeyCapture()
|
2020-12-21 03:13:03 +01:00
|
|
|
|
}
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
runOnMainLooperForStreamingEvent {
|
2021-05-17 07:03:18 +02:00
|
|
|
|
if (is_dispose.get()) return@runOnMainLooperForStreamingEvent
|
2020-12-22 00:49:26 +01:00
|
|
|
|
fireShowColumnStatus()
|
2020-12-06 21:19:02 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-22 05:27:39 +01:00
|
|
|
|
override fun onTimelineItem(item: TimelineItem, channelId: String?, stream: JsonArray?) {
|
2021-05-17 07:03:18 +02:00
|
|
|
|
if (StreamManager.traceDelivery) log.v("${access_info.acct} onTimelineItem")
|
2020-12-22 05:27:39 +01:00
|
|
|
|
if (!canHandleStreamingMessage()) return
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
when (item) {
|
|
|
|
|
is TootConversationSummary -> {
|
|
|
|
|
if (type != ColumnType.DIRECT_MESSAGES) return
|
|
|
|
|
if (isFiltered(item.last_status)) return
|
|
|
|
|
if (use_old_api) {
|
|
|
|
|
useConversationSummaryStreaming = false
|
|
|
|
|
return
|
|
|
|
|
} else {
|
|
|
|
|
useConversationSummaryStreaming = true
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
is TootNotification -> {
|
|
|
|
|
if (!isNotificationColumn) return
|
|
|
|
|
if (isFiltered(item)) return
|
|
|
|
|
}
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
is TootStatus -> {
|
|
|
|
|
if (isNotificationColumn) return
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
// マストドン2.6.0形式のDMカラム用イベントを利用したならば、その直後に発生する普通の投稿イベントを無視する
|
|
|
|
|
if (useConversationSummaryStreaming) return
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
// マストドンはLTLに外部ユーザの投稿を表示しない
|
|
|
|
|
if (type == ColumnType.LOCAL && isMastodon && item.account.isRemote) return
|
2020-12-21 03:13:03 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
if (isFiltered(item)) return
|
|
|
|
|
}
|
2020-12-21 03:13:03 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
stream_data_queue.add(item)
|
|
|
|
|
app_state.handler.post(mergeStreamingMessage)
|
|
|
|
|
}
|
2020-12-21 03:13:03 +01:00
|
|
|
|
|
2021-05-17 07:03:18 +02:00
|
|
|
|
override fun onEmojiReaction(item: TootNotification) {
|
|
|
|
|
// 自分によるリアクションは通知されない
|
|
|
|
|
// リアクション削除は通知されない
|
|
|
|
|
runOnMainLooperForStreamingEvent {
|
|
|
|
|
updateEmojiReaction(item.status)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
override fun onNoteUpdated(ev: MisskeyNoteUpdate, channelId: String?) {
|
|
|
|
|
runOnMainLooperForStreamingEvent {
|
2020-12-21 03:13:03 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
// userId が自分かどうか調べる
|
|
|
|
|
// アクセストークンの更新をして自分のuserIdが分かる状態でないとキャプチャ結果を反映させない
|
|
|
|
|
// (でないとリアクションの2重カウントなどが発生してしまう)
|
|
|
|
|
val myId = EntityId.from(access_info.token_info, TootApiClient.KEY_USER_ID)
|
|
|
|
|
if (myId == null) {
|
|
|
|
|
log.w("onNoteUpdated: missing my userId. updating access token is recommenced!!")
|
|
|
|
|
}
|
2020-12-21 03:13:03 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
val byMe = myId == ev.userId
|
2020-12-21 03:13:03 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
val changeList = ArrayList<AdapterChange>()
|
2020-12-21 03:13:03 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
fun scanStatus1(s: TootStatus?, idx: Int, block: (s: TootStatus) -> Boolean) {
|
|
|
|
|
s ?: return
|
|
|
|
|
if (s.id == ev.noteId) {
|
|
|
|
|
if (block(s)) {
|
|
|
|
|
changeList.add(AdapterChange(AdapterChangeType.RangeChange, idx, 1))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
scanStatus1(s.reblog, idx, block)
|
|
|
|
|
scanStatus1(s.reply, idx, block)
|
|
|
|
|
}
|
2020-12-21 03:13:03 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
fun scanStatusAll(block: (s: TootStatus) -> Boolean) {
|
|
|
|
|
for (i in 0 until list_data.size) {
|
|
|
|
|
val o = list_data[i]
|
|
|
|
|
if (o is TootStatus) {
|
|
|
|
|
scanStatus1(o, i, block)
|
|
|
|
|
} else if (o is TootNotification) {
|
|
|
|
|
scanStatus1(o.status, i, block)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-12-21 03:13:03 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
when (ev.type) {
|
|
|
|
|
MisskeyNoteUpdate.Type.REACTION -> {
|
|
|
|
|
scanStatusAll { s ->
|
2021-05-17 07:03:18 +02:00
|
|
|
|
s.increaseReaction(
|
|
|
|
|
true,
|
|
|
|
|
ev.reaction,
|
|
|
|
|
byMe,
|
|
|
|
|
ev.emoji,
|
|
|
|
|
"onNoteUpdated ${ev.userId}"
|
|
|
|
|
)
|
2020-12-22 00:49:26 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-12-21 03:13:03 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
MisskeyNoteUpdate.Type.UNREACTION -> {
|
|
|
|
|
scanStatusAll { s ->
|
2021-05-17 07:03:18 +02:00
|
|
|
|
s.decreaseReaction(
|
|
|
|
|
true,
|
|
|
|
|
ev.reaction,
|
|
|
|
|
byMe,
|
|
|
|
|
"onNoteUpdated ${ev.userId}"
|
|
|
|
|
)
|
2020-12-22 00:49:26 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-12-21 03:13:03 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
MisskeyNoteUpdate.Type.VOTED -> {
|
|
|
|
|
scanStatusAll { s ->
|
|
|
|
|
s.enquete?.increaseVote(context, ev.choice, byMe) ?: false
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-12-21 03:13:03 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
MisskeyNoteUpdate.Type.DELETED -> {
|
|
|
|
|
scanStatusAll { s ->
|
|
|
|
|
s.markDeleted(context, ev.deletedAt)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (changeList.isNotEmpty()) {
|
|
|
|
|
fireShowContent(reason = "onNoteUpdated", changeList = changeList)
|
|
|
|
|
}
|
2020-12-21 03:13:03 +01:00
|
|
|
|
}
|
2020-12-22 00:49:26 +01:00
|
|
|
|
}
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
override fun onAnnouncementUpdate(item: TootAnnouncement) {
|
|
|
|
|
runOnMainLooperForStreamingEvent {
|
2020-12-22 05:27:39 +01:00
|
|
|
|
if (type != ColumnType.HOME)
|
2020-12-22 00:49:26 +01:00
|
|
|
|
return@runOnMainLooperForStreamingEvent
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
val list = announcements
|
|
|
|
|
if (list == null) {
|
|
|
|
|
announcements = mutableListOf(item)
|
|
|
|
|
} else {
|
|
|
|
|
val index = list.indexOfFirst { it.id == item.id }
|
|
|
|
|
list.add(
|
|
|
|
|
0,
|
|
|
|
|
if (index == -1) {
|
|
|
|
|
item
|
|
|
|
|
} else {
|
|
|
|
|
TootAnnouncement.merge(list.removeAt(index), item)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
announcementUpdated = SystemClock.elapsedRealtime()
|
|
|
|
|
fireShowColumnHeader()
|
2020-12-06 21:19:02 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-12-21 21:16:33 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
override fun onAnnouncementDelete(id: EntityId) {
|
|
|
|
|
runOnMainLooperForStreamingEvent {
|
2020-12-22 05:27:39 +01:00
|
|
|
|
announcements?.iterator()?.let {
|
2020-12-22 00:49:26 +01:00
|
|
|
|
while (it.hasNext()) {
|
|
|
|
|
val item = it.next()
|
|
|
|
|
if (item.id != id) continue
|
|
|
|
|
it.remove()
|
|
|
|
|
announcementUpdated = SystemClock.elapsedRealtime()
|
|
|
|
|
fireShowColumnHeader()
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-12-21 21:16:33 +01:00
|
|
|
|
|
2021-05-17 07:03:18 +02:00
|
|
|
|
override fun onAnnouncementReaction(reaction: TootReaction) {
|
2020-12-22 00:49:26 +01:00
|
|
|
|
runOnMainLooperForStreamingEvent {
|
|
|
|
|
// find announcement
|
|
|
|
|
val announcement_id = reaction.announcement_id
|
|
|
|
|
?: return@runOnMainLooperForStreamingEvent
|
|
|
|
|
val announcement = announcements?.find { it.id == announcement_id }
|
|
|
|
|
?: return@runOnMainLooperForStreamingEvent
|
2020-12-21 21:16:33 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
// find reaction
|
|
|
|
|
val index = announcement.reactions?.indexOfFirst { it.name == reaction.name }
|
|
|
|
|
when {
|
|
|
|
|
reaction.count <= 0L -> {
|
|
|
|
|
if (index != null && index != -1) announcement.reactions?.removeAt(index)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
index == null -> {
|
2021-05-17 07:03:18 +02:00
|
|
|
|
announcement.reactions = ArrayList<TootReaction>().apply {
|
2020-12-22 00:49:26 +01:00
|
|
|
|
add(reaction)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
index == -1 -> announcement.reactions?.add(reaction)
|
|
|
|
|
|
|
|
|
|
else -> announcement.reactions?.get(index)?.let { old ->
|
|
|
|
|
old.count = reaction.count
|
|
|
|
|
// ストリーミングイベントにはmeが含まれないので、oldにあるmeは変更されない
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
announcementUpdated = SystemClock.elapsedRealtime()
|
|
|
|
|
fireShowColumnHeader()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-22 05:27:39 +01:00
|
|
|
|
private val mergeStreamingMessage = object : Runnable {
|
2020-12-22 00:49:26 +01:00
|
|
|
|
override fun run() {
|
|
|
|
|
val handler = app_state.handler
|
2020-12-22 05:27:39 +01:00
|
|
|
|
|
|
|
|
|
// 未初期化や初期ロード中ならキューをクリアして何もしない
|
2021-05-17 07:03:18 +02:00
|
|
|
|
if (!canHandleStreamingMessage()) {
|
2020-12-22 05:27:39 +01:00
|
|
|
|
stream_data_queue.clear()
|
2020-12-22 00:49:26 +01:00
|
|
|
|
handler.removeCallbacks(this)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-22 05:27:39 +01:00
|
|
|
|
// 前回マージしてから暫くは待機してリトライ
|
2020-12-22 00:49:26 +01:00
|
|
|
|
// カラムがビジー状態なら待機してリトライ
|
2020-12-22 05:27:39 +01:00
|
|
|
|
val now = SystemClock.elapsedRealtime()
|
|
|
|
|
var remain = last_show_stream_data.get() + 333L - now
|
|
|
|
|
if (bRefreshLoading) remain = max(333L, remain)
|
|
|
|
|
if (remain > 0) {
|
2020-12-22 00:49:26 +01:00
|
|
|
|
handler.removeCallbacks(this)
|
2020-12-22 05:27:39 +01:00
|
|
|
|
handler.postDelayed(this, remain)
|
2020-12-22 00:49:26 +01:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
last_show_stream_data.set(now)
|
|
|
|
|
|
|
|
|
|
val tmpList = ArrayList<TimelineItem>()
|
|
|
|
|
while (true) tmpList.add(stream_data_queue.poll() ?: break)
|
|
|
|
|
if (tmpList.isEmpty()) return
|
|
|
|
|
|
|
|
|
|
// キューから読めた件数が0の場合を除き、少し後に再処理させることでマージ漏れを防ぐ
|
|
|
|
|
handler.postDelayed(this, 333L)
|
|
|
|
|
|
|
|
|
|
// ストリーミングされるデータは全てID順に並んでいるはず
|
|
|
|
|
tmpList.sortByDescending { it.getOrderId() }
|
|
|
|
|
|
|
|
|
|
val list_new = duplicate_map.filterDuplicate(tmpList)
|
|
|
|
|
if (list_new.isEmpty()) return
|
|
|
|
|
|
|
|
|
|
for (item in list_new) {
|
|
|
|
|
if (enable_speech && item is TootStatus) {
|
|
|
|
|
app_state.addSpeech(item.reblog ?: item)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 通知カラムならストリーミング経由で届いたデータを通知ワーカーに伝達する
|
|
|
|
|
if (isNotificationColumn) {
|
|
|
|
|
val list = ArrayList<TootNotification>()
|
|
|
|
|
for (o in list_new) {
|
|
|
|
|
if (o is TootNotification) {
|
|
|
|
|
list.add(o)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (list.isNotEmpty()) {
|
|
|
|
|
PollingWorker.injectData(context, access_info, list)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 最新のIDをsince_idとして覚える(ソートはしない)
|
|
|
|
|
var new_id_max: EntityId? = null
|
|
|
|
|
var new_id_min: EntityId? = null
|
|
|
|
|
for (o in list_new) {
|
|
|
|
|
try {
|
|
|
|
|
val id = o.getOrderId()
|
|
|
|
|
if (id.toString().isEmpty()) continue
|
|
|
|
|
if (new_id_max == null || id > new_id_max) new_id_max = id
|
|
|
|
|
if (new_id_min == null || id < new_id_min) new_id_min = id
|
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
|
// IDを取得できないタイプのオブジェクトだった
|
|
|
|
|
// ストリームに来るのは通知かステータスだから、多分ここは通らない
|
|
|
|
|
log.trace(ex)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val tmpRecent = idRecent
|
|
|
|
|
val tmpNewMax = new_id_max
|
|
|
|
|
|
|
|
|
|
if (tmpNewMax != null && (tmpRecent?.compareTo(tmpNewMax) ?: -1) == -1) {
|
|
|
|
|
idRecent = tmpNewMax
|
|
|
|
|
// XXX: コレはリフレッシュ時に取得漏れを引き起こすのでは…?
|
|
|
|
|
// しかしコレなしだとリフレッシュ時に大量に読むことになる…
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val holder = viewHolder
|
|
|
|
|
|
|
|
|
|
// 事前にスクロール位置を覚えておく
|
|
|
|
|
val holder_sp: ScrollPosition? = holder?.scrollPosition
|
|
|
|
|
|
|
|
|
|
// idx番目の要素がListViewの上端から何ピクセル下にあるか
|
|
|
|
|
var restore_idx = -2
|
|
|
|
|
var restore_y = 0
|
|
|
|
|
if (holder != null) {
|
|
|
|
|
if (list_data.size > 0) {
|
|
|
|
|
try {
|
|
|
|
|
restore_idx = holder.findFirstVisibleListItem()
|
|
|
|
|
restore_y = holder.getListItemOffset(restore_idx)
|
|
|
|
|
} catch (ex: IndexOutOfBoundsException) {
|
|
|
|
|
restore_idx = -2
|
|
|
|
|
restore_y = 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 画面復帰時の自動リフレッシュではギャップが残る可能性がある
|
|
|
|
|
if (bPutGap) {
|
|
|
|
|
bPutGap = false
|
|
|
|
|
try {
|
|
|
|
|
if (list_data.size > 0 && new_id_min != null) {
|
|
|
|
|
val since = list_data[0].getOrderId()
|
|
|
|
|
if (new_id_min > since) {
|
|
|
|
|
val gap = TootGap(new_id_min, since)
|
|
|
|
|
list_new.add(gap)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
|
log.e(ex, "can't put gap.")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val changeList = ArrayList<AdapterChange>()
|
|
|
|
|
|
|
|
|
|
replaceConversationSummary(changeList, list_new, list_data)
|
|
|
|
|
|
|
|
|
|
val added = list_new.size // may 0
|
|
|
|
|
|
|
|
|
|
var doneSound = false
|
|
|
|
|
for (o in list_new) {
|
|
|
|
|
if (o is TootStatus) {
|
|
|
|
|
o.highlightSound?.let {
|
|
|
|
|
if (!doneSound) {
|
|
|
|
|
doneSound = true
|
|
|
|
|
App1.sound(it)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
o.highlightSpeech?.let {
|
|
|
|
|
app_state.addSpeech(it.name, dedupMode = DedupMode.RecentExpire)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
changeList.add(AdapterChange(AdapterChangeType.RangeInsert, 0, added))
|
|
|
|
|
list_data.addAll(0, list_new)
|
|
|
|
|
|
|
|
|
|
fireShowContent(reason = "mergeStreamingMessage", changeList = changeList)
|
|
|
|
|
|
|
|
|
|
if (holder != null) {
|
|
|
|
|
when {
|
|
|
|
|
holder_sp == null -> {
|
|
|
|
|
// スクロール位置が先頭なら先頭にする
|
|
|
|
|
log.d("mergeStreamingMessage: has VH. missing scroll position.")
|
|
|
|
|
viewHolder?.scrollToTop()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
holder_sp.isHead -> {
|
|
|
|
|
// スクロール位置が先頭なら先頭にする
|
|
|
|
|
log.d("mergeStreamingMessage: has VH. keep head. $holder_sp")
|
|
|
|
|
holder.setScrollPosition(ScrollPosition())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
restore_idx < -1 -> {
|
|
|
|
|
// 可視範囲の検出に失敗
|
|
|
|
|
log.d("mergeStreamingMessage: has VH. can't get visible range.")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
else -> {
|
|
|
|
|
// 現在の要素が表示され続けるようにしたい
|
|
|
|
|
log.d("mergeStreamingMessage: has VH. added=$added")
|
|
|
|
|
holder.setListItemTop(restore_idx + added, restore_y)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
val scroll_save = this@Column.scroll_save
|
|
|
|
|
when {
|
|
|
|
|
|
|
|
|
|
// スクロール位置が先頭なら先頭のまま
|
|
|
|
|
scroll_save == null || scroll_save.isHead -> {
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 現在の要素が表示され続けるようにしたい
|
|
|
|
|
else -> scroll_save.adapterIndex += added
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateMisskeyCapture()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
|
|
internal constructor(
|
|
|
|
|
app_state: AppState,
|
|
|
|
|
access_info: SavedAccount,
|
|
|
|
|
type: Int,
|
|
|
|
|
vararg params: Any
|
|
|
|
|
) : this(
|
|
|
|
|
app_state, app_state.context, access_info, type, generateColumnId()
|
|
|
|
|
) {
|
|
|
|
|
when (typeMap[type]) {
|
|
|
|
|
|
|
|
|
|
ColumnType.CONVERSATION,
|
|
|
|
|
ColumnType.BOOSTED_BY,
|
|
|
|
|
ColumnType.FAVOURITED_BY,
|
|
|
|
|
ColumnType.LOCAL_AROUND,
|
|
|
|
|
ColumnType.FEDERATED_AROUND,
|
|
|
|
|
ColumnType.ACCOUNT_AROUND ->
|
|
|
|
|
status_id = getParamEntityId(params, 0)
|
|
|
|
|
|
|
|
|
|
ColumnType.PROFILE, ColumnType.LIST_TL, ColumnType.LIST_MEMBER,
|
|
|
|
|
ColumnType.MISSKEY_ANTENNA_TL ->
|
|
|
|
|
profile_id = getParamEntityId(params, 0)
|
|
|
|
|
|
|
|
|
|
ColumnType.HASHTAG ->
|
|
|
|
|
hashtag = getParamString(params, 0)
|
|
|
|
|
|
|
|
|
|
ColumnType.HASHTAG_FROM_ACCT -> {
|
|
|
|
|
hashtag = getParamString(params, 0)
|
|
|
|
|
hashtag_acct = getParamString(params, 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ColumnType.NOTIFICATION_FROM_ACCT -> {
|
|
|
|
|
hashtag_acct = getParamString(params, 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ColumnType.SEARCH -> {
|
|
|
|
|
search_query = getParamString(params, 0)
|
|
|
|
|
search_resolve = getParamAt(params, 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ColumnType.SEARCH_MSP, ColumnType.SEARCH_TS, ColumnType.SEARCH_NOTESTOCK ->
|
|
|
|
|
search_query = getParamString(params, 0)
|
|
|
|
|
|
|
|
|
|
ColumnType.INSTANCE_INFORMATION ->
|
|
|
|
|
instance_uri = getParamString(params, 0)
|
|
|
|
|
|
|
|
|
|
ColumnType.PROFILE_DIRECTORY -> {
|
|
|
|
|
instance_uri = getParamString(params, 0)
|
|
|
|
|
search_resolve = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ColumnType.DOMAIN_TIMELINE -> {
|
|
|
|
|
instance_uri = getParamString(params, 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
else -> {
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal constructor(app_state: AppState, src: JsonObject)
|
|
|
|
|
: this(
|
|
|
|
|
app_state,
|
|
|
|
|
app_state.context,
|
|
|
|
|
loadAccount(app_state.context, src),
|
|
|
|
|
src.optInt(KEY_TYPE),
|
|
|
|
|
decodeColumnId(src)
|
|
|
|
|
) {
|
|
|
|
|
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)
|
2020-12-21 21:16:33 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
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)
|
|
|
|
|
}
|
2020-12-21 21:16:33 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
ColumnType.DOMAIN_TIMELINE -> {
|
|
|
|
|
instance_uri = src.optString(KEY_INSTANCE_URI)
|
|
|
|
|
}
|
2020-12-21 21:16:33 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
else -> {
|
2020-12-21 21:16:33 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
2020-12-06 21:19:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
override fun hashCode(): Int = internalId
|
|
|
|
|
|
|
|
|
|
override fun equals(other: Any?): Boolean = this === other
|
|
|
|
|
|
|
|
|
|
fun getIconId(): Int = type.iconId(access_info.acct)
|
|
|
|
|
|
|
|
|
|
fun getColumnName(long: Boolean) =
|
|
|
|
|
type.name2(this, long) ?: type.name1(context)
|
|
|
|
|
|
2020-12-06 21:19:02 +01:00
|
|
|
|
@Throws(JsonException::class)
|
|
|
|
|
fun encodeJSON(dst: JsonObject, old_index: Int) {
|
|
|
|
|
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) {
|
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
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
|
|
|
|
|
}
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal fun isSameSpec(
|
2020-12-21 03:13:03 +01:00
|
|
|
|
ai: SavedAccount,
|
|
|
|
|
type: ColumnType,
|
|
|
|
|
params: Array<out Any>
|
|
|
|
|
): Boolean {
|
2020-12-06 21:19:02 +01:00
|
|
|
|
if (type != this.type || ai != access_info) return false
|
|
|
|
|
|
|
|
|
|
return try {
|
|
|
|
|
when (type) {
|
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
ColumnType.PROFILE,
|
|
|
|
|
ColumnType.LIST_TL, ColumnType.LIST_MEMBER,
|
|
|
|
|
ColumnType.MISSKEY_ANTENNA_TL ->
|
|
|
|
|
profile_id == getParamEntityId(params, 0)
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
ColumnType.CONVERSATION, ColumnType.BOOSTED_BY, ColumnType.FAVOURITED_BY, ColumnType.LOCAL_AROUND, ColumnType.FEDERATED_AROUND, ColumnType.ACCOUNT_AROUND ->
|
|
|
|
|
status_id == getParamEntityId(params, 0)
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
ColumnType.HASHTAG -> {
|
|
|
|
|
(getParamString(params, 0) == hashtag)
|
|
|
|
|
&& ((getParamAtNullable<String>(params, 1) ?: "") == hashtag_any)
|
|
|
|
|
&& ((getParamAtNullable<String>(params, 2) ?: "") == hashtag_all)
|
|
|
|
|
&& ((getParamAtNullable<String>(params, 3) ?: "") == hashtag_none)
|
|
|
|
|
}
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
ColumnType.HASHTAG_FROM_ACCT -> {
|
|
|
|
|
(getParamString(params, 0) == hashtag)
|
|
|
|
|
&& ((getParamAtNullable<String>(params, 1) ?: "") == hashtag_acct)
|
|
|
|
|
}
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
ColumnType.NOTIFICATION_FROM_ACCT -> {
|
|
|
|
|
((getParamAtNullable<String>(params, 0) ?: "") == hashtag_acct)
|
|
|
|
|
}
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
ColumnType.SEARCH ->
|
|
|
|
|
getParamString(params, 0) == search_query &&
|
|
|
|
|
getParamAtNullable<Boolean>(params, 1) == search_resolve
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
ColumnType.SEARCH_MSP, ColumnType.SEARCH_TS, ColumnType.SEARCH_NOTESTOCK ->
|
|
|
|
|
getParamString(params, 0) == search_query
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
ColumnType.INSTANCE_INFORMATION -> getParamString(params, 0) == instance_uri
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
ColumnType.PROFILE_DIRECTORY ->
|
|
|
|
|
getParamString(params, 0) == instance_uri &&
|
|
|
|
|
getParamAtNullable<String>(params, 1) == search_query &&
|
|
|
|
|
getParamAtNullable<Boolean>(params, 2) == search_resolve
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
ColumnType.DOMAIN_TIMELINE ->
|
|
|
|
|
getParamString(params, 0) == instance_uri
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
|
|
|
|
else -> true
|
|
|
|
|
}
|
|
|
|
|
} catch (ex: Throwable) {
|
|
|
|
|
log.trace(ex)
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun getNotificationTypeString(): String {
|
|
|
|
|
val sb = StringBuilder()
|
|
|
|
|
sb.append("(")
|
|
|
|
|
|
|
|
|
|
when (quick_filter) {
|
2020-12-21 03:13:03 +01:00
|
|
|
|
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 "" // 全部か皆無なら部分表記は要らない
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QUICK_FILTER_MENTION -> sb.append(context.getString(R.string.notification_type_mention))
|
|
|
|
|
QUICK_FILTER_FAVOURITE -> sb.append(context.getString(R.string.notification_type_favourite))
|
|
|
|
|
QUICK_FILTER_BOOST -> sb.append(context.getString(R.string.notification_type_boost))
|
|
|
|
|
QUICK_FILTER_FOLLOW -> sb.append(context.getString(R.string.notification_type_follow))
|
|
|
|
|
QUICK_FILTER_REACTION -> sb.append(context.getString(R.string.notification_type_reaction))
|
|
|
|
|
QUICK_FILTER_VOTE -> sb.append(context.getString(R.string.notification_type_vote))
|
|
|
|
|
QUICK_FILTER_POST -> sb.append(context.getString(R.string.notification_type_post))
|
2020-12-06 21:19:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sb.append(")")
|
|
|
|
|
return sb.toString()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal fun dispose() {
|
|
|
|
|
is_dispose.set(true)
|
2020-12-21 03:13:03 +01:00
|
|
|
|
app_state.streamManager.updateStreamingColumns()
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
|
|
|
|
for (vh in _holder_list) {
|
|
|
|
|
try {
|
|
|
|
|
vh.listView.adapter = null
|
|
|
|
|
} catch (ignored: Throwable) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ブーストやお気に入りの更新に使う。ステータスを列挙する。
|
|
|
|
|
fun findStatus(
|
2020-12-21 03:13:03 +01:00
|
|
|
|
target_apDomain: Host,
|
|
|
|
|
target_status_id: EntityId,
|
|
|
|
|
callback: (account: SavedAccount, status: TootStatus) -> Boolean
|
|
|
|
|
// callback return true if rebind view required
|
|
|
|
|
) {
|
2020-12-06 21:19:02 +01:00
|
|
|
|
if (!access_info.matchHost(target_apDomain)) return
|
|
|
|
|
|
|
|
|
|
var bChanged = false
|
|
|
|
|
|
|
|
|
|
fun procStatus(status: TootStatus?) {
|
|
|
|
|
if (status != null) {
|
|
|
|
|
if (target_status_id == status.id) {
|
|
|
|
|
if (callback(access_info, status)) bChanged = true
|
|
|
|
|
}
|
|
|
|
|
procStatus(status.reblog)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (data in list_data) {
|
|
|
|
|
when (data) {
|
2020-12-21 03:13:03 +01:00
|
|
|
|
is TootNotification -> procStatus(data.status)
|
|
|
|
|
is TootStatus -> procStatus(data)
|
2020-12-06 21:19:02 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (bChanged) fireRebindAdapterItems()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ミュート、ブロックが成功した時に呼ばれる
|
|
|
|
|
// リストメンバーカラムでメンバーをリストから除去した時に呼ばれる
|
|
|
|
|
fun removeAccountInTimeline(
|
2020-12-21 03:13:03 +01:00
|
|
|
|
target_account: SavedAccount,
|
|
|
|
|
who_id: EntityId,
|
|
|
|
|
removeFromUserList: Boolean = false
|
|
|
|
|
) {
|
2020-12-06 21:19:02 +01:00
|
|
|
|
if (target_account != access_info) return
|
|
|
|
|
|
|
|
|
|
val INVALID_ACCOUNT = -1L
|
|
|
|
|
|
|
|
|
|
val tmp_list = ArrayList<TimelineItem>(list_data.size)
|
|
|
|
|
for (o in list_data) {
|
|
|
|
|
if (o is TootStatus) {
|
|
|
|
|
if (who_id == (o.account.id)) continue
|
|
|
|
|
if (who_id == (o.reblog?.account?.id ?: INVALID_ACCOUNT)) continue
|
|
|
|
|
} else if (o is TootNotification) {
|
|
|
|
|
if (who_id == (o.account?.id ?: INVALID_ACCOUNT)) continue
|
|
|
|
|
if (who_id == (o.status?.account?.id ?: INVALID_ACCOUNT)) continue
|
|
|
|
|
if (who_id == (o.status?.reblog?.account?.id ?: INVALID_ACCOUNT)) continue
|
|
|
|
|
} else if (o is TootAccountRef && removeFromUserList) {
|
|
|
|
|
if (who_id == o.get().id) continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tmp_list.add(o)
|
|
|
|
|
}
|
|
|
|
|
if (tmp_list.size != list_data.size) {
|
|
|
|
|
list_data.clear()
|
|
|
|
|
list_data.addAll(tmp_list)
|
|
|
|
|
fireShowContent(reason = "removeAccountInTimeline")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ミュート、ブロックが成功した時に呼ばれる
|
|
|
|
|
// リストメンバーカラムでメンバーをリストから除去した時に呼ばれる
|
|
|
|
|
// require full acct
|
|
|
|
|
fun removeAccountInTimelinePseudo(acct: Acct) {
|
|
|
|
|
|
|
|
|
|
val tmp_list = ArrayList<TimelineItem>(list_data.size)
|
|
|
|
|
for (o in list_data) {
|
|
|
|
|
if (o is TootStatus) {
|
|
|
|
|
if (acct == access_info.getFullAcct(o.account)) continue
|
|
|
|
|
if (acct == access_info.getFullAcct(o.reblog?.account)) continue
|
|
|
|
|
} else if (o is TootNotification) {
|
|
|
|
|
if (acct == access_info.getFullAcct(o.account)) continue
|
|
|
|
|
if (acct == access_info.getFullAcct(o.status?.account)) continue
|
|
|
|
|
if (acct == access_info.getFullAcct(o.status?.reblog?.account)) continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tmp_list.add(o)
|
|
|
|
|
}
|
|
|
|
|
if (tmp_list.size != list_data.size) {
|
|
|
|
|
list_data.clear()
|
|
|
|
|
list_data.addAll(tmp_list)
|
|
|
|
|
fireShowContent(reason = "removeAccountInTimelinePseudo")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// misskeyカラムやプロフカラムでブロック成功した時に呼ばれる
|
|
|
|
|
fun updateFollowIcons(target_account: SavedAccount) {
|
|
|
|
|
if (target_account != access_info) return
|
|
|
|
|
|
|
|
|
|
fireShowContent(reason = "updateFollowIcons", reset = true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun removeUser(targetAccount: SavedAccount, columnType: ColumnType, who_id: EntityId) {
|
|
|
|
|
if (type == columnType && targetAccount == access_info) {
|
|
|
|
|
val tmp_list = ArrayList<TimelineItem>(list_data.size)
|
|
|
|
|
for (o in list_data) {
|
|
|
|
|
if (o is TootAccountRef) {
|
|
|
|
|
if (o.get().id == who_id) continue
|
|
|
|
|
}
|
|
|
|
|
tmp_list.add(o)
|
|
|
|
|
}
|
|
|
|
|
if (tmp_list.size != list_data.size) {
|
|
|
|
|
list_data.clear()
|
|
|
|
|
list_data.addAll(tmp_list)
|
|
|
|
|
fireShowContent(reason = "removeUser")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ステータスが削除された時に呼ばれる
|
|
|
|
|
fun onStatusRemoved(tl_host: Host, status_id: EntityId) {
|
|
|
|
|
|
|
|
|
|
if (is_dispose.get() || bInitialLoading || bRefreshLoading) return
|
|
|
|
|
|
|
|
|
|
if (!access_info.matchHost(tl_host)) return
|
|
|
|
|
|
|
|
|
|
val tmp_list = ArrayList<TimelineItem>(list_data.size)
|
|
|
|
|
for (o in list_data) {
|
|
|
|
|
if (o is TootStatus) {
|
|
|
|
|
if (status_id == o.id) continue
|
|
|
|
|
if (status_id == (o.reblog?.id ?: -1L)) continue
|
|
|
|
|
} else if (o is TootNotification) {
|
|
|
|
|
val s = o.status
|
|
|
|
|
if (s != null) {
|
|
|
|
|
if (status_id == s.id) continue
|
|
|
|
|
if (status_id == (s.reblog?.id ?: -1L)) continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tmp_list.add(o)
|
|
|
|
|
}
|
|
|
|
|
if (tmp_list.size != list_data.size) {
|
|
|
|
|
list_data.clear()
|
|
|
|
|
list_data.addAll(tmp_list)
|
|
|
|
|
fireShowContent(reason = "removeStatus")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun removeNotifications() {
|
|
|
|
|
cancelLastTask()
|
|
|
|
|
|
|
|
|
|
mRefreshLoadingErrorPopupState = 0
|
|
|
|
|
mRefreshLoadingError = ""
|
|
|
|
|
bRefreshLoading = false
|
|
|
|
|
mInitialLoadingError = ""
|
|
|
|
|
bInitialLoading = false
|
|
|
|
|
idOld = null
|
|
|
|
|
idRecent = null
|
|
|
|
|
offsetNext = 0
|
|
|
|
|
pagingType = ColumnPagingType.Default
|
|
|
|
|
|
|
|
|
|
list_data.clear()
|
|
|
|
|
duplicate_map.clear()
|
|
|
|
|
fireShowContent(reason = "removeNotifications", reset = true)
|
|
|
|
|
|
|
|
|
|
PollingWorker.queueNotificationCleared(context, access_info.db_id)
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
|
2020-12-06 21:19:02 +01:00
|
|
|
|
fun removeNotificationOne(target_account: SavedAccount, notification: TootNotification) {
|
|
|
|
|
if (!isNotificationColumn) return
|
|
|
|
|
|
|
|
|
|
if (access_info != target_account) return
|
|
|
|
|
|
|
|
|
|
val tmp_list = ArrayList<TimelineItem>(list_data.size)
|
|
|
|
|
for (o in list_data) {
|
|
|
|
|
if (o is TootNotification) {
|
|
|
|
|
if (o.id == notification.id) continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tmp_list.add(o)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (tmp_list.size != list_data.size) {
|
|
|
|
|
list_data.clear()
|
|
|
|
|
list_data.addAll(tmp_list)
|
|
|
|
|
fireShowContent(reason = "removeNotificationOne")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun onMuteUpdated() {
|
|
|
|
|
|
|
|
|
|
val checker = { status: TootStatus? -> status?.checkMuted() ?: false }
|
|
|
|
|
|
|
|
|
|
val tmp_list = ArrayList<TimelineItem>(list_data.size)
|
|
|
|
|
for (o in list_data) {
|
|
|
|
|
if (o is TootStatus) {
|
|
|
|
|
if (checker(o)) continue
|
|
|
|
|
}
|
|
|
|
|
if (o is TootNotification) {
|
|
|
|
|
if (checker(o.status)) continue
|
|
|
|
|
}
|
|
|
|
|
tmp_list.add(o)
|
|
|
|
|
}
|
|
|
|
|
if (tmp_list.size != list_data.size) {
|
|
|
|
|
list_data.clear()
|
|
|
|
|
list_data.addAll(tmp_list)
|
|
|
|
|
fireShowContent(reason = "onMuteUpdated")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun onHideFavouriteNotification(acct: Acct) {
|
|
|
|
|
if (!isNotificationColumn) return
|
|
|
|
|
|
|
|
|
|
val tmp_list = ArrayList<TimelineItem>(list_data.size)
|
|
|
|
|
|
|
|
|
|
for (o in list_data) {
|
|
|
|
|
if (o is TootNotification && o.type != TootNotification.TYPE_MENTION) {
|
|
|
|
|
val a = o.account
|
|
|
|
|
if (a != null) {
|
|
|
|
|
val a_acct = access_info.getFullAcct(a)
|
|
|
|
|
if (a_acct == acct) continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
tmp_list.add(o)
|
|
|
|
|
}
|
|
|
|
|
if (tmp_list.size != list_data.size) {
|
|
|
|
|
list_data.clear()
|
|
|
|
|
list_data.addAll(tmp_list)
|
|
|
|
|
fireShowContent(reason = "onHideFavouriteNotification")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun onDomainBlockChanged(
|
2020-12-21 03:13:03 +01:00
|
|
|
|
target_account: SavedAccount,
|
|
|
|
|
domain: Host,
|
|
|
|
|
bBlocked: Boolean
|
|
|
|
|
) {
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
|
|
|
|
if (target_account.apiHost != access_info.apiHost) return
|
|
|
|
|
if (access_info.isPseudo) return
|
|
|
|
|
|
|
|
|
|
if (type == ColumnType.DOMAIN_BLOCKS) {
|
|
|
|
|
// ドメインブロック一覧を読み直す
|
|
|
|
|
startLoading()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (bBlocked) {
|
|
|
|
|
// ブロックしたのとドメイン部分が一致するアカウントからのステータスと通知をすべて除去する
|
|
|
|
|
val checker =
|
|
|
|
|
{ account: TootAccount? -> if (account == null) false else account.acct.host == domain }
|
|
|
|
|
|
|
|
|
|
val tmp_list = ArrayList<TimelineItem>(list_data.size)
|
|
|
|
|
|
|
|
|
|
for (o in list_data) {
|
|
|
|
|
if (o is TootStatus) {
|
|
|
|
|
if (checker(o.account)) continue
|
|
|
|
|
if (checker(o.reblog?.account)) continue
|
|
|
|
|
} else if (o is TootNotification) {
|
|
|
|
|
if (checker(o.account)) continue
|
|
|
|
|
if (checker(o.status?.account)) continue
|
|
|
|
|
if (checker(o.status?.reblog?.account)) continue
|
|
|
|
|
}
|
|
|
|
|
tmp_list.add(o)
|
|
|
|
|
}
|
|
|
|
|
if (tmp_list.size != list_data.size) {
|
|
|
|
|
list_data.clear()
|
|
|
|
|
list_data.addAll(tmp_list)
|
|
|
|
|
fireShowContent(reason = "onDomainBlockChanged")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun onListListUpdated(account: SavedAccount) {
|
|
|
|
|
if (account != access_info) return
|
|
|
|
|
if (type == ColumnType.LIST_LIST || type == ColumnType.MISSKEY_ANTENNA_LIST) {
|
|
|
|
|
startLoading()
|
|
|
|
|
val vh = viewHolder
|
|
|
|
|
vh?.onListListUpdated()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun onListNameUpdated(account: SavedAccount, item: TootList) {
|
|
|
|
|
if (account != access_info) return
|
|
|
|
|
if (type == ColumnType.LIST_LIST) {
|
|
|
|
|
startLoading()
|
|
|
|
|
} else if (type == ColumnType.LIST_TL || type == ColumnType.LIST_MEMBER) {
|
|
|
|
|
if (item.id == profile_id) {
|
|
|
|
|
this.list_info = item
|
|
|
|
|
fireShowColumnHeader()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// fun onAntennaNameUpdated(account : SavedAccount, item : MisskeyAntenna) {
|
|
|
|
|
// if(account != access_info) return
|
|
|
|
|
// if(type == ColumnType.MISSKEY_ANTENNA_LIST) {
|
|
|
|
|
// startLoading()
|
|
|
|
|
// } else if(type == ColumnType.MISSKEY_ANTENNA_TL) {
|
|
|
|
|
// if(item.id == profile_id) {
|
|
|
|
|
// this.antenna_info = item
|
|
|
|
|
// fireShowColumnHeader()
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
fun onListMemberUpdated(
|
2020-12-21 03:13:03 +01:00
|
|
|
|
account: SavedAccount,
|
|
|
|
|
list_id: EntityId,
|
|
|
|
|
who: TootAccount,
|
|
|
|
|
bAdd: Boolean
|
|
|
|
|
) {
|
2020-12-06 21:19:02 +01:00
|
|
|
|
if (type == ColumnType.LIST_TL && access_info == account && list_id == profile_id) {
|
|
|
|
|
if (!bAdd) {
|
|
|
|
|
removeAccountInTimeline(account, who.id)
|
|
|
|
|
}
|
|
|
|
|
} else if (type == ColumnType.LIST_MEMBER && access_info == account && list_id == profile_id) {
|
|
|
|
|
if (!bAdd) {
|
|
|
|
|
removeAccountInTimeline(account, who.id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun onLanguageFilterChanged() {
|
|
|
|
|
// TODO
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal fun addColumnViewHolder(cvh: ColumnViewHolder) {
|
|
|
|
|
|
|
|
|
|
// 現在のリストにあるなら削除する
|
|
|
|
|
removeColumnViewHolder(cvh)
|
|
|
|
|
|
|
|
|
|
// 最後に追加されたものが先頭にくるようにする
|
|
|
|
|
// 呼び出しの後に必ず追加されているようにする
|
|
|
|
|
_holder_list.addFirst(cvh)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal fun removeColumnViewHolder(cvh: ColumnViewHolder) {
|
|
|
|
|
val it = _holder_list.iterator()
|
|
|
|
|
while (it.hasNext()) {
|
|
|
|
|
if (cvh == it.next()) it.remove()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal fun removeColumnViewHolderByActivity(activity: ActMain) {
|
|
|
|
|
val it = _holder_list.iterator()
|
|
|
|
|
while (it.hasNext()) {
|
|
|
|
|
val cvh = it.next()
|
|
|
|
|
if (cvh.activity == activity) {
|
|
|
|
|
it.remove()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal fun hasMultipleViewHolder(): Boolean {
|
|
|
|
|
return _holder_list.size > 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal fun fireShowContent(
|
2020-12-21 03:13:03 +01:00
|
|
|
|
reason: String,
|
|
|
|
|
changeList: List<AdapterChange>? = null,
|
|
|
|
|
reset: Boolean = false
|
|
|
|
|
) {
|
2020-12-06 21:19:02 +01:00
|
|
|
|
if (!isMainThread) {
|
|
|
|
|
throw RuntimeException("fireShowContent: not on main thread.")
|
|
|
|
|
}
|
|
|
|
|
viewHolder?.showContent(reason, changeList, reset)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal fun fireShowColumnHeader() {
|
|
|
|
|
if (!isMainThread) {
|
|
|
|
|
throw RuntimeException("fireShowColumnHeader: not on main thread.")
|
|
|
|
|
}
|
|
|
|
|
viewHolder?.showColumnHeader()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal fun fireShowColumnStatus() {
|
|
|
|
|
if (!isMainThread) {
|
|
|
|
|
throw RuntimeException("fireShowColumnStatus: not on main thread.")
|
|
|
|
|
}
|
|
|
|
|
viewHolder?.showColumnStatus()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal fun fireColumnColor() {
|
|
|
|
|
if (!isMainThread) {
|
|
|
|
|
throw RuntimeException("fireColumnColor: not on main thread.")
|
|
|
|
|
}
|
|
|
|
|
viewHolder?.showColumnColor()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun fireRelativeTime() {
|
|
|
|
|
if (!isMainThread) {
|
|
|
|
|
throw RuntimeException("fireRelativeTime: not on main thread.")
|
|
|
|
|
}
|
|
|
|
|
viewHolder?.updateRelativeTime()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun fireRebindAdapterItems() {
|
|
|
|
|
if (!isMainThread) {
|
|
|
|
|
throw RuntimeException("fireRelativeTime: not on main thread.")
|
|
|
|
|
}
|
|
|
|
|
viewHolder?.rebindAdapterItems()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun cancelLastTask() {
|
|
|
|
|
if (lastTask != null) {
|
|
|
|
|
lastTask?.cancel()
|
|
|
|
|
lastTask = null
|
|
|
|
|
//
|
|
|
|
|
bInitialLoading = false
|
|
|
|
|
bRefreshLoading = false
|
|
|
|
|
mInitialLoadingError = context.getString(R.string.cancelled)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun initFilter() {
|
|
|
|
|
column_regex_filter = 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) {
|
|
|
|
|
log.trace(ex)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
favMuteSet = FavMute.acctSet
|
|
|
|
|
highlight_trie = HighlightWord.nameSet
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun 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 isFiltered(status: TootStatus): Boolean {
|
|
|
|
|
|
|
|
|
|
val filterTrees = keywordFilterTrees
|
|
|
|
|
if (filterTrees != null) {
|
|
|
|
|
if (status.isKeywordFiltered(access_info, filterTrees.treeIrreversible)) {
|
|
|
|
|
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 checkLanguageFilter(status: TootStatus?): Boolean {
|
|
|
|
|
status ?: return false
|
|
|
|
|
val languageFilter = language_filter ?: return false
|
|
|
|
|
|
|
|
|
|
val allow = languageFilter.boolean(
|
2020-12-21 03:13:03 +01:00
|
|
|
|
status.language ?: status.reblog?.language ?: TootStatus.LANGUAGE_CODE_UNKNOWN
|
|
|
|
|
)
|
2020-12-06 21:19:02 +01:00
|
|
|
|
?: languageFilter.boolean(TootStatus.LANGUAGE_CODE_DEFAULT)
|
|
|
|
|
?: true
|
|
|
|
|
|
|
|
|
|
return !allow
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal fun isFiltered(item: TootNotification): Boolean {
|
|
|
|
|
|
|
|
|
|
if (when (quick_filter) {
|
2020-12-21 03:13:03 +01:00
|
|
|
|
QUICK_FILTER_ALL -> when (item.type) {
|
|
|
|
|
TootNotification.TYPE_FAVOURITE -> dont_show_favourite
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
TootNotification.TYPE_REBLOG,
|
|
|
|
|
TootNotification.TYPE_RENOTE,
|
|
|
|
|
TootNotification.TYPE_QUOTE -> dont_show_boost
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
TootNotification.TYPE_FOLLOW,
|
|
|
|
|
TootNotification.TYPE_UNFOLLOW,
|
|
|
|
|
TootNotification.TYPE_FOLLOW_REQUEST,
|
|
|
|
|
TootNotification.TYPE_FOLLOW_REQUEST_MISSKEY,
|
|
|
|
|
TootNotification.TYPE_FOLLOW_REQUEST_ACCEPTED_MISSKEY -> dont_show_follow
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
TootNotification.TYPE_MENTION,
|
|
|
|
|
TootNotification.TYPE_REPLY -> dont_show_reply
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2021-05-17 07:03:18 +02:00
|
|
|
|
TootNotification.TYPE_EMOJI_REACTION,
|
2020-12-21 03:13:03 +01:00
|
|
|
|
TootNotification.TYPE_REACTION -> dont_show_reaction
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
TootNotification.TYPE_VOTE,
|
|
|
|
|
TootNotification.TYPE_POLL,
|
|
|
|
|
TootNotification.TYPE_POLL_VOTE_MISSKEY -> dont_show_vote
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
TootNotification.TYPE_STATUS -> dont_show_normal_toot
|
|
|
|
|
else -> false
|
|
|
|
|
}
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
|
|
|
|
else -> when (item.type) {
|
2020-12-21 03:13:03 +01:00
|
|
|
|
TootNotification.TYPE_FAVOURITE -> quick_filter != QUICK_FILTER_FAVOURITE
|
|
|
|
|
TootNotification.TYPE_REBLOG,
|
|
|
|
|
TootNotification.TYPE_RENOTE,
|
|
|
|
|
TootNotification.TYPE_QUOTE -> quick_filter != 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 != QUICK_FILTER_FOLLOW
|
|
|
|
|
|
|
|
|
|
TootNotification.TYPE_MENTION,
|
|
|
|
|
TootNotification.TYPE_REPLY -> quick_filter != QUICK_FILTER_MENTION
|
2021-05-17 07:03:18 +02:00
|
|
|
|
|
|
|
|
|
TootNotification.TYPE_EMOJI_REACTION,
|
2020-12-21 03:13:03 +01:00
|
|
|
|
TootNotification.TYPE_REACTION -> quick_filter != QUICK_FILTER_REACTION
|
|
|
|
|
|
|
|
|
|
TootNotification.TYPE_VOTE,
|
|
|
|
|
TootNotification.TYPE_POLL,
|
|
|
|
|
TootNotification.TYPE_POLL_VOTE_MISSKEY -> quick_filter != QUICK_FILTER_VOTE
|
|
|
|
|
|
|
|
|
|
TootNotification.TYPE_STATUS -> quick_filter != QUICK_FILTER_POST
|
2020-12-06 21:19:02 +01:00
|
|
|
|
else -> true
|
|
|
|
|
}
|
2021-05-17 07:03:18 +02:00
|
|
|
|
}
|
|
|
|
|
) {
|
2020-12-06 21:19:02 +01:00
|
|
|
|
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)) {
|
|
|
|
|
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) {
|
|
|
|
|
log.d("isFiltered: status muted by in-app muted words.")
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ふぁぼ魔ミュート
|
|
|
|
|
when (item.type) {
|
2020-12-21 03:13:03 +01:00
|
|
|
|
TootNotification.TYPE_REBLOG,
|
|
|
|
|
TootNotification.TYPE_RENOTE,
|
|
|
|
|
TootNotification.TYPE_QUOTE,
|
|
|
|
|
TootNotification.TYPE_FAVOURITE,
|
2021-05-17 07:03:18 +02:00
|
|
|
|
TootNotification.TYPE_EMOJI_REACTION,
|
2020-12-21 03:13:03 +01:00
|
|
|
|
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) {
|
|
|
|
|
log.d("%s is in favMuteSet.", access_info.getFullAcct(who))
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-12-06 21:19:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// @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;
|
|
|
|
|
// }
|
|
|
|
|
|
2020-12-08 21:24:42 +01:00
|
|
|
|
internal suspend fun loadProfileAccount(
|
2020-12-21 03:13:03 +01:00
|
|
|
|
client: TootApiClient,
|
|
|
|
|
parser: TootParser,
|
|
|
|
|
bForceReload: Boolean
|
|
|
|
|
): TootApiResult? {
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
|
|
|
|
return if (this.who_account != null && !bForceReload) {
|
|
|
|
|
// リロード不要なら何もしない
|
|
|
|
|
null
|
|
|
|
|
} else if (isMisskey) {
|
|
|
|
|
client.request(
|
2020-12-21 03:13:03 +01:00
|
|
|
|
PATH_MISSKEY_PROFILE,
|
|
|
|
|
access_info.putMisskeyApiToken().apply {
|
|
|
|
|
put("userId", profile_id)
|
|
|
|
|
}.toPostRequestBuilder()
|
|
|
|
|
)?.also { result1 ->
|
2020-12-06 21:19:02 +01:00
|
|
|
|
// ユーザリレーションの取り扱いのため、別のparserを作ってはいけない
|
|
|
|
|
parser.misskeyDecodeProfilePin = true
|
|
|
|
|
try {
|
|
|
|
|
TootAccountRef.mayNull(parser, parser.account(result1.jsonObject))?.also { a ->
|
|
|
|
|
this.who_account = a
|
|
|
|
|
client.publishApiProgress("") // カラムヘッダの再表示
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
parser.misskeyDecodeProfilePin = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
client.request(String.format(Locale.JAPAN, PATH_ACCOUNT, profile_id))?.also { result1 ->
|
|
|
|
|
TootAccountRef.mayNull(parser, parser.account(result1.jsonObject))?.also { a ->
|
|
|
|
|
this.who_account = a
|
2020-12-06 19:32:21 +01:00
|
|
|
|
|
2020-12-06 21:19:02 +01:00
|
|
|
|
this.who_featured_tags = null
|
2021-05-17 07:03:18 +02:00
|
|
|
|
client.request("/api/v1/accounts/${profile_id}/featured_tags")
|
|
|
|
|
?.also { result2 ->
|
2020-12-06 19:32:21 +01:00
|
|
|
|
|
2021-05-17 07:03:18 +02:00
|
|
|
|
this.who_featured_tags =
|
|
|
|
|
TootTag.parseListOrNull(parser, result2.jsonArray)
|
|
|
|
|
}
|
2020-12-06 19:32:21 +01:00
|
|
|
|
|
2020-12-06 21:19:02 +01:00
|
|
|
|
client.publishApiProgress("") // カラムヘッダの再表示
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private inner class UpdateRelationEnv {
|
|
|
|
|
|
|
|
|
|
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 = 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)
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-08 21:24:42 +01:00
|
|
|
|
suspend fun update(client: TootApiClient, parser: TootParser) {
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
|
|
|
|
var n: Int
|
|
|
|
|
var size: Int
|
|
|
|
|
|
|
|
|
|
if (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 > RELATIONSHIP_LOAD_STEP) step = RELATIONSHIP_LOAD_STEP
|
|
|
|
|
UserRelation.saveListMisskey(now, access_info.db_id, who_list, start, step)
|
|
|
|
|
start += step
|
|
|
|
|
}
|
|
|
|
|
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>(RELATIONSHIP_LOAD_STEP)
|
|
|
|
|
for (i in 0 until 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(
|
2020-12-21 03:13:03 +01:00
|
|
|
|
"/api/users/relation",
|
|
|
|
|
access_info.putMisskeyApiToken().apply {
|
|
|
|
|
put(
|
|
|
|
|
"userId",
|
|
|
|
|
userIdList.map { it.toString() }.toJsonArray()
|
|
|
|
|
)
|
|
|
|
|
}.toPostRequestBuilder()
|
|
|
|
|
)
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
|
|
|
|
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, access_info.db_id, list)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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 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(
|
2020-12-21 03:13:03 +01:00
|
|
|
|
now,
|
|
|
|
|
access_info.db_id,
|
|
|
|
|
list
|
|
|
|
|
)
|
2020-12-06 21:19:02 +01:00
|
|
|
|
}
|
|
|
|
|
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 > ACCT_DB_STEP) length = ACCT_DB_STEP
|
|
|
|
|
AcctSet.saveList(now, acct_list, n, length)
|
|
|
|
|
n += length
|
|
|
|
|
}
|
|
|
|
|
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 > ACCT_DB_STEP) length = ACCT_DB_STEP
|
|
|
|
|
TagSet.saveList(now, tag_list, n, length)
|
|
|
|
|
n += length
|
|
|
|
|
}
|
|
|
|
|
log.d("updateRelation: update %d tag.", n)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//
|
2020-12-08 21:24:42 +01:00
|
|
|
|
internal suspend fun updateRelation(
|
2020-12-21 03:13:03 +01:00
|
|
|
|
client: TootApiClient,
|
|
|
|
|
list: ArrayList<TimelineItem>?,
|
|
|
|
|
whoRef: TootAccountRef?,
|
|
|
|
|
parser: TootParser
|
|
|
|
|
) {
|
2020-12-06 21:19:02 +01:00
|
|
|
|
if (access_info.isPseudo) return
|
|
|
|
|
|
|
|
|
|
val env = UpdateRelationEnv()
|
|
|
|
|
|
|
|
|
|
env.add(whoRef)
|
|
|
|
|
|
|
|
|
|
list?.forEach {
|
|
|
|
|
when (it) {
|
2020-12-21 03:13:03 +01:00
|
|
|
|
is TootAccountRef -> env.add(it)
|
|
|
|
|
is TootStatus -> env.add(it)
|
|
|
|
|
is TootNotification -> env.add(it)
|
|
|
|
|
is TootConversationSummary -> env.add(it.last_status)
|
2020-12-06 21:19:02 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
env.update(client, parser)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
internal fun startLoading() {
|
|
|
|
|
cancelLastTask()
|
|
|
|
|
|
|
|
|
|
initFilter()
|
|
|
|
|
|
|
|
|
|
showOpenSticker = Pref.bpOpenSticker(app_state.pref)
|
|
|
|
|
|
|
|
|
|
mRefreshLoadingErrorPopupState = 0
|
|
|
|
|
mRefreshLoadingError = ""
|
|
|
|
|
mInitialLoadingError = ""
|
|
|
|
|
bFirstInitialized = true
|
|
|
|
|
bInitialLoading = true
|
|
|
|
|
bRefreshLoading = false
|
|
|
|
|
idOld = null
|
|
|
|
|
idRecent = null
|
|
|
|
|
offsetNext = 0
|
|
|
|
|
pagingType = ColumnPagingType.Default
|
|
|
|
|
|
|
|
|
|
duplicate_map.clear()
|
|
|
|
|
list_data.clear()
|
|
|
|
|
fireShowContent(reason = "loading start", reset = true)
|
|
|
|
|
|
|
|
|
|
@SuppressLint("StaticFieldLeak")
|
|
|
|
|
val task = ColumnTask_Loading(this)
|
|
|
|
|
this.lastTask = task
|
|
|
|
|
task.start()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal fun parseRange(
|
2020-12-21 03:13:03 +01:00
|
|
|
|
result: TootApiResult?,
|
|
|
|
|
list: List<TimelineItem>?
|
|
|
|
|
): Pair<EntityId?, EntityId?> {
|
2020-12-06 21:19:02 +01:00
|
|
|
|
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 = reMaxId.matcher(result?.link_older ?: "").findOrNull()
|
|
|
|
|
?.let {
|
|
|
|
|
EntityId(it.groupEx(1)!!)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
idMax = 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
|
|
|
|
|
internal fun saveRange(
|
2020-12-21 03:13:03 +01:00
|
|
|
|
bBottom: Boolean,
|
|
|
|
|
bTop: Boolean,
|
|
|
|
|
result: TootApiResult?,
|
|
|
|
|
list: List<TimelineItem>?
|
|
|
|
|
): Boolean {
|
|
|
|
|
val (idMin, idMax) = parseRange(result, list)
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
|
|
|
|
var hasBottomRemain = false
|
|
|
|
|
|
|
|
|
|
if (bBottom) when (idMin) {
|
2020-12-21 03:13:03 +01:00
|
|
|
|
null -> idOld = null // リストの終端
|
2020-12-06 21:19:02 +01:00
|
|
|
|
else -> {
|
|
|
|
|
val i = idOld?.compareTo(idMin)
|
|
|
|
|
if (i == null || i > 0) {
|
|
|
|
|
idOld = idMin
|
|
|
|
|
hasBottomRemain = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (bTop) when (idMax) {
|
2020-12-21 03:13:03 +01:00
|
|
|
|
null -> {
|
|
|
|
|
// リロードを許容するため、取得内容がカラでもidRecentを変更しない
|
|
|
|
|
}
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
|
|
|
|
else -> {
|
|
|
|
|
val i = idRecent?.compareTo(idMax)
|
|
|
|
|
if (i == null || i < 0) {
|
|
|
|
|
idRecent = idMax
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return hasBottomRemain
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// return true if list bottom may have unread remain
|
|
|
|
|
internal fun saveRangeBottom(result: TootApiResult?, list: List<TimelineItem>?) =
|
|
|
|
|
saveRange(true, bTop = false, result = result, list = list)
|
|
|
|
|
|
|
|
|
|
// return true if list bottom may have unread remain
|
|
|
|
|
internal fun saveRangeTop(result: TootApiResult?, list: List<TimelineItem>?) =
|
|
|
|
|
saveRange(false, bTop = true, result = result, list = list)
|
|
|
|
|
|
|
|
|
|
internal fun addRange(
|
2020-12-21 03:13:03 +01:00
|
|
|
|
bBottom: Boolean,
|
|
|
|
|
path: String,
|
|
|
|
|
delimiter: Char = if (-1 == path.indexOf('?')) '?' else '&'
|
|
|
|
|
) = if (bBottom) {
|
2020-12-06 21:19:02 +01:00
|
|
|
|
if (idOld != null) "$path${delimiter}max_id=${idOld}" else path
|
|
|
|
|
} else {
|
|
|
|
|
if (idRecent != null) "$path${delimiter}since_id=${idRecent}" else path
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal fun addRangeMin(
|
2020-12-21 03:13:03 +01:00
|
|
|
|
path: String,
|
|
|
|
|
delimiter: Char = if (-1 != path.indexOf('?')) '&' else '?'
|
|
|
|
|
) = if (idRecent == null) path else "$path${delimiter}min_id=${idRecent}"
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
|
|
|
|
internal fun startRefreshForPost(
|
2020-12-21 03:13:03 +01:00
|
|
|
|
refresh_after_post: Int,
|
|
|
|
|
posted_status_id: EntityId,
|
|
|
|
|
posted_reply_id: EntityId?
|
|
|
|
|
) {
|
2020-12-06 21:19:02 +01:00
|
|
|
|
when (type) {
|
2020-12-21 03:13:03 +01:00
|
|
|
|
ColumnType.HOME, ColumnType.LOCAL, ColumnType.FEDERATE, ColumnType.DIRECT_MESSAGES, ColumnType.MISSKEY_HYBRID -> {
|
|
|
|
|
startRefresh(
|
|
|
|
|
bSilent = true,
|
|
|
|
|
bBottom = false,
|
|
|
|
|
posted_status_id = posted_status_id,
|
|
|
|
|
refresh_after_toot = refresh_after_post
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ColumnType.PROFILE -> {
|
|
|
|
|
if (profile_tab == ProfileTab.Status
|
|
|
|
|
&& profile_id == access_info.loginAccount?.id
|
|
|
|
|
) {
|
|
|
|
|
startRefresh(
|
|
|
|
|
bSilent = true,
|
|
|
|
|
bBottom = false,
|
|
|
|
|
posted_status_id = posted_status_id,
|
|
|
|
|
refresh_after_toot = refresh_after_post
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ColumnType.CONVERSATION -> {
|
|
|
|
|
// 会話への返信が行われたなら会話を更新する
|
|
|
|
|
try {
|
|
|
|
|
if (posted_reply_id != null) {
|
|
|
|
|
for (item in list_data) {
|
|
|
|
|
if (item is TootStatus && item.id == posted_reply_id) {
|
|
|
|
|
startLoading()
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (_: Throwable) {
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
|
|
|
|
else -> {
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal fun startRefresh(
|
2020-12-21 03:13:03 +01:00
|
|
|
|
bSilent: Boolean,
|
|
|
|
|
bBottom: Boolean,
|
|
|
|
|
posted_status_id: EntityId? = null,
|
|
|
|
|
refresh_after_toot: Int = -1
|
|
|
|
|
) {
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
|
|
|
|
if (lastTask != null) {
|
|
|
|
|
if (!bSilent) {
|
|
|
|
|
context.showToast(true, R.string.column_is_busy)
|
|
|
|
|
val holder = viewHolder
|
|
|
|
|
if (holder != null) holder.refreshLayout.isRefreshing = false
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
} else if (bBottom && !canRefreshBottom()) {
|
|
|
|
|
if (!bSilent) {
|
|
|
|
|
context.showToast(true, R.string.end_of_list)
|
|
|
|
|
val holder = viewHolder
|
|
|
|
|
if (holder != null) holder.refreshLayout.isRefreshing = false
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
} else if (!bBottom && !canRefreshTop()) {
|
|
|
|
|
val holder = viewHolder
|
|
|
|
|
if (holder != null) holder.refreshLayout.isRefreshing = false
|
|
|
|
|
startLoading()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (bSilent) {
|
|
|
|
|
val holder = viewHolder
|
|
|
|
|
if (holder != null) {
|
|
|
|
|
holder.refreshLayout.isRefreshing = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!bBottom) {
|
|
|
|
|
bRefreshingTop = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bRefreshLoading = true
|
|
|
|
|
mRefreshLoadingError = ""
|
|
|
|
|
|
|
|
|
|
@SuppressLint("StaticFieldLeak")
|
|
|
|
|
val task = ColumnTask_Refresh(this, bSilent, bBottom, posted_status_id, refresh_after_toot)
|
|
|
|
|
this.lastTask = task
|
|
|
|
|
task.start()
|
|
|
|
|
fireShowColumnStatus()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal fun startGap(gap: TimelineItem?, isHead: Boolean) {
|
|
|
|
|
|
|
|
|
|
if (gap == null) {
|
|
|
|
|
context.showToast(true, "gap is null")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (lastTask != null) {
|
|
|
|
|
context.showToast(true, R.string.column_is_busy)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Suppress("UNNECESSARY_SAFE_CALL")
|
|
|
|
|
viewHolder?.refreshLayout?.isRefreshing = true
|
|
|
|
|
|
|
|
|
|
bRefreshLoading = true
|
|
|
|
|
mRefreshLoadingError = ""
|
|
|
|
|
|
|
|
|
|
@SuppressLint("StaticFieldLeak")
|
|
|
|
|
val task = ColumnTask_Gap(this, gap, isHead = isHead)
|
|
|
|
|
this.lastTask = task
|
|
|
|
|
task.start()
|
|
|
|
|
fireShowColumnStatus()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun toAdapterIndex(listIndex: Int): Int {
|
|
|
|
|
return if (type.headerType != null) listIndex + 1 else listIndex
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun toListIndex(adapterIndex: Int): Int {
|
|
|
|
|
return if (type.headerType != null) adapterIndex - 1 else adapterIndex
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun loadSearchDesc(raw_en: Int, raw_ja: Int): String {
|
|
|
|
|
val res_id = if ("ja" == context.getString(R.string.language_code)) raw_ja else raw_en
|
|
|
|
|
return context.loadRawResource(res_id).decodeUTF8()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2020-12-08 21:24:42 +01:00
|
|
|
|
fun getHeaderDesc(): String {
|
2020-12-06 21:19:02 +01:00
|
|
|
|
var cache = cacheHeaderDesc
|
|
|
|
|
if (cache != null) return cache
|
|
|
|
|
cache = when (type) {
|
2020-12-21 03:13:03 +01:00
|
|
|
|
ColumnType.SEARCH -> context.getString(R.string.search_desc_mastodon_api)
|
|
|
|
|
ColumnType.SEARCH_MSP -> loadSearchDesc(
|
|
|
|
|
R.raw.search_desc_msp_en,
|
|
|
|
|
R.raw.search_desc_msp_ja
|
|
|
|
|
)
|
|
|
|
|
ColumnType.SEARCH_TS -> loadSearchDesc(
|
|
|
|
|
R.raw.search_desc_ts_en,
|
|
|
|
|
R.raw.search_desc_ts_ja
|
|
|
|
|
)
|
2020-12-07 13:53:55 +01:00
|
|
|
|
ColumnType.SEARCH_NOTESTOCK -> loadSearchDesc(
|
|
|
|
|
R.raw.search_desc_notestock_en,
|
|
|
|
|
R.raw.search_desc_notestock_ja
|
|
|
|
|
)
|
2020-12-06 21:19:02 +01:00
|
|
|
|
else -> ""
|
|
|
|
|
}
|
|
|
|
|
cacheHeaderDesc = cache
|
|
|
|
|
return cache
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////
|
|
|
|
|
// Streaming
|
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
internal fun onStart() {
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
|
|
|
|
// 破棄されたカラムなら何もしない
|
|
|
|
|
if (is_dispose.get()) {
|
|
|
|
|
log.d("onStart: column was disposed.")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 未初期化なら何もしない
|
|
|
|
|
if (!bFirstInitialized) {
|
|
|
|
|
log.d("onStart: column is not initialized.")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 初期ロード中なら何もしない
|
|
|
|
|
if (bInitialLoading) {
|
|
|
|
|
log.d("onStart: column is in initial loading.")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// フィルタ一覧のリロードが必要
|
|
|
|
|
if (filter_reload_required) {
|
|
|
|
|
filter_reload_required = false
|
|
|
|
|
startLoading()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 始端リフレッシュの最中だった
|
|
|
|
|
// リフレッシュ終了時に自動でストリーミング開始するはず
|
|
|
|
|
if (bRefreshingTop) {
|
|
|
|
|
log.d("onStart: bRefreshingTop is true.")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!bRefreshLoading
|
|
|
|
|
&& canAutoRefresh()
|
2020-12-21 03:13:03 +01:00
|
|
|
|
&& !Pref.bpDontRefreshOnResume(app_state.pref)
|
2020-12-06 21:19:02 +01:00
|
|
|
|
&& !dont_auto_refresh
|
|
|
|
|
) {
|
|
|
|
|
// リフレッシュしてからストリーミング開始
|
|
|
|
|
log.d("onStart: start auto refresh.")
|
|
|
|
|
startRefresh(bSilent = true, bBottom = false)
|
|
|
|
|
} else if (isSearchColumn) {
|
|
|
|
|
// 検索カラムはリフレッシュもストリーミングもないが、表示開始のタイミングでリストの再描画を行いたい
|
|
|
|
|
fireShowContent(reason = "Column onStart isSearchColumn", reset = true)
|
2020-12-22 05:27:39 +01:00
|
|
|
|
} else if (canStartStreaming() && streamSpec != null) {
|
2020-12-06 21:19:02 +01:00
|
|
|
|
// ギャップつきでストリーミング開始
|
2020-12-22 00:49:26 +01:00
|
|
|
|
this.bPutGap = true
|
|
|
|
|
fireShowColumnStatus()
|
2020-12-06 21:19:02 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// カラム設定に正規表現フィルタを含めるなら真
|
|
|
|
|
fun canStatusFilter(): Boolean {
|
|
|
|
|
if (getFilterContext() != TootFilter.CONTEXT_NONE) return true
|
|
|
|
|
|
|
|
|
|
return when (type) {
|
2020-12-21 03:13:03 +01:00
|
|
|
|
ColumnType.SEARCH_MSP, ColumnType.SEARCH_TS, ColumnType.SEARCH_NOTESTOCK -> true
|
2020-12-06 21:19:02 +01:00
|
|
|
|
else -> false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// マストドン2.4.3rcのキーワードフィルタのコンテキスト
|
|
|
|
|
private fun getFilterContext() = when (type) {
|
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
ColumnType.HOME, ColumnType.LIST_TL, ColumnType.MISSKEY_HYBRID -> TootFilter.CONTEXT_HOME
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
ColumnType.NOTIFICATIONS, ColumnType.NOTIFICATION_FROM_ACCT -> TootFilter.CONTEXT_NOTIFICATIONS
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
ColumnType.CONVERSATION -> TootFilter.CONTEXT_THREAD
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
ColumnType.DIRECT_MESSAGES -> TootFilter.CONTEXT_THREAD
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
ColumnType.PROFILE -> TootFilter.CONTEXT_PROFILE
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
|
|
|
|
else -> TootFilter.CONTEXT_PUBLIC
|
|
|
|
|
// ColumnType.MISSKEY_HYBRID や ColumnType.MISSKEY_ANTENNA_TL はHOMEでもPUBLICでもある…
|
|
|
|
|
// Misskeyだし関係ないが、NONEにするとアプリ内で完結するフィルタも働かなくなる
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// カラム設定に「すべての画像を隠す」ボタンを含めるなら真
|
|
|
|
|
internal fun canNSFWDefault(): Boolean {
|
|
|
|
|
return canStatusFilter()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun canRemoteOnly() = when (type) {
|
2020-12-21 03:13:03 +01:00
|
|
|
|
ColumnType.FEDERATE, ColumnType.FEDERATED_AROUND -> true
|
2020-12-06 21:19:02 +01:00
|
|
|
|
else -> false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// カラム設定に「ブーストを表示しない」ボタンを含めるなら真
|
|
|
|
|
fun canFilterBoost(): Boolean {
|
|
|
|
|
return when (type) {
|
2020-12-21 03:13:03 +01:00
|
|
|
|
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
|
2020-12-06 21:19:02 +01:00
|
|
|
|
else -> false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// カラム設定に「返信を表示しない」ボタンを含めるなら真
|
|
|
|
|
fun canFilterReply(): Boolean {
|
|
|
|
|
return when (type) {
|
2020-12-21 03:13:03 +01:00
|
|
|
|
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
|
2020-12-06 21:19:02 +01:00
|
|
|
|
else -> false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun canFilterNormalToot(): Boolean {
|
|
|
|
|
return when (type) {
|
2020-12-21 03:13:03 +01:00
|
|
|
|
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
|
2020-12-06 21:19:02 +01:00
|
|
|
|
else -> false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun canFilterNonPublicToot(): Boolean {
|
|
|
|
|
return when (type) {
|
2020-12-21 03:13:03 +01:00
|
|
|
|
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
|
2020-12-06 21:19:02 +01:00
|
|
|
|
else -> false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
|
2020-12-06 21:19:02 +01:00
|
|
|
|
internal fun hasHashtagExtra() = when {
|
|
|
|
|
isMisskey -> false
|
|
|
|
|
type == ColumnType.HASHTAG -> true
|
|
|
|
|
|
|
|
|
|
// ColumnType.HASHTAG_FROM_ACCT は追加のタグを指定しても結果に反映されない
|
|
|
|
|
|
|
|
|
|
else -> false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun StringBuilder.appendHashtagExtra(): StringBuilder {
|
|
|
|
|
val limit = (HASHTAG_ELLIPSIZE * 2 - min(length, HASHTAG_ELLIPSIZE)) / 3
|
|
|
|
|
if (hashtag_any.isNotBlank()) append(' ').append(
|
2020-12-21 03:13:03 +01:00
|
|
|
|
context.getString(
|
|
|
|
|
R.string.hashtag_title_any,
|
|
|
|
|
hashtag_any.ellipsizeDot3(limit)
|
|
|
|
|
)
|
|
|
|
|
)
|
2020-12-06 21:19:02 +01:00
|
|
|
|
if (hashtag_all.isNotBlank()) append(' ').append(
|
2020-12-21 03:13:03 +01:00
|
|
|
|
context.getString(
|
|
|
|
|
R.string.hashtag_title_all,
|
|
|
|
|
hashtag_all.ellipsizeDot3(limit)
|
|
|
|
|
)
|
|
|
|
|
)
|
2020-12-06 21:19:02 +01:00
|
|
|
|
if (hashtag_none.isNotBlank()) append(' ').append(
|
2020-12-21 03:13:03 +01:00
|
|
|
|
context.getString(
|
|
|
|
|
R.string.hashtag_title_none,
|
|
|
|
|
hashtag_none.ellipsizeDot3(limit)
|
|
|
|
|
)
|
|
|
|
|
)
|
2020-12-06 21:19:02 +01:00
|
|
|
|
return this
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun canReloadWhenRefreshTop(): Boolean {
|
|
|
|
|
return when (type) {
|
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
ColumnType.KEYWORD_FILTER,
|
|
|
|
|
ColumnType.SEARCH,
|
|
|
|
|
ColumnType.SEARCH_MSP,
|
|
|
|
|
ColumnType.SEARCH_TS,
|
2020-12-07 13:53:55 +01:00
|
|
|
|
ColumnType.SEARCH_NOTESTOCK,
|
2020-12-21 03:13:03 +01:00
|
|
|
|
ColumnType.CONVERSATION,
|
|
|
|
|
ColumnType.LIST_LIST,
|
|
|
|
|
ColumnType.TREND_TAG,
|
|
|
|
|
ColumnType.FOLLOW_SUGGESTION,
|
|
|
|
|
ColumnType.PROFILE_DIRECTORY -> true
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
ColumnType.LIST_MEMBER,
|
|
|
|
|
ColumnType.MUTES,
|
|
|
|
|
ColumnType.FOLLOW_REQUESTS -> isMisskey
|
|
|
|
|
|
|
|
|
|
else -> false
|
2020-12-06 21:19:02 +01:00
|
|
|
|
}
|
2020-12-22 00:49:26 +01:00
|
|
|
|
}
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
// カラム操作的にリフレッシュを許容するかどうか
|
|
|
|
|
fun canRefreshTopBySwipe(): Boolean {
|
|
|
|
|
return canReloadWhenRefreshTop() || when (type) {
|
|
|
|
|
ColumnType.CONVERSATION,
|
|
|
|
|
ColumnType.INSTANCE_INFORMATION -> false
|
|
|
|
|
else -> true
|
2020-12-06 21:19:02 +01:00
|
|
|
|
}
|
2020-12-22 00:49:26 +01:00
|
|
|
|
}
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
// カラム操作的にリフレッシュを許容するかどうか
|
|
|
|
|
fun canRefreshBottomBySwipe(): Boolean {
|
|
|
|
|
return when (type) {
|
|
|
|
|
ColumnType.LIST_LIST,
|
|
|
|
|
ColumnType.CONVERSATION,
|
|
|
|
|
ColumnType.INSTANCE_INFORMATION,
|
|
|
|
|
ColumnType.KEYWORD_FILTER,
|
|
|
|
|
ColumnType.SEARCH,
|
|
|
|
|
ColumnType.TREND_TAG,
|
|
|
|
|
ColumnType.FOLLOW_SUGGESTION -> false
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
ColumnType.FOLLOW_REQUESTS -> isMisskey
|
2020-12-21 21:16:33 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
ColumnType.LIST_MEMBER -> !isMisskey
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
else -> true
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
2020-12-22 00:49:26 +01:00
|
|
|
|
// データ的にリフレッシュを許容するかどうか
|
|
|
|
|
private fun canRefreshTop(): Boolean {
|
|
|
|
|
return when (pagingType) {
|
|
|
|
|
ColumnPagingType.Default -> idRecent != null
|
|
|
|
|
else -> false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// データ的にリフレッシュを許容するかどうか
|
|
|
|
|
private fun canRefreshBottom(): Boolean {
|
|
|
|
|
return when (pagingType) {
|
|
|
|
|
ColumnPagingType.Default, ColumnPagingType.Cursor -> idOld != null
|
|
|
|
|
ColumnPagingType.None -> false
|
|
|
|
|
ColumnPagingType.Offset -> true
|
2020-12-06 21:19:02 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
// 別スレッドから呼ばれるが大丈夫か
|
|
|
|
|
fun canStartStreaming() = when {
|
2020-12-06 21:19:02 +01:00
|
|
|
|
// 未初期化なら何もしない
|
2020-12-21 03:13:03 +01:00
|
|
|
|
!bFirstInitialized -> {
|
2021-05-17 07:03:18 +02:00
|
|
|
|
if (StreamManager.traceDelivery) log.v("canStartStreaming: column is not initialized.")
|
2020-12-21 03:13:03 +01:00
|
|
|
|
false
|
2020-12-06 21:19:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 初期ロード中なら何もしない
|
2020-12-21 03:13:03 +01:00
|
|
|
|
bInitialLoading -> {
|
2021-05-17 07:03:18 +02:00
|
|
|
|
if (StreamManager.traceDelivery) log.v("canStartStreaming: is in initial loading.")
|
2020-12-21 03:13:03 +01:00
|
|
|
|
false
|
2020-12-06 21:19:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
else -> true
|
|
|
|
|
}
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private fun min(a: Int, b: Int): Int = if (a < b) a else b
|
|
|
|
|
|
|
|
|
|
internal fun updateMisskeyCapture() {
|
|
|
|
|
if (!isMisskey) return
|
2020-12-21 03:13:03 +01:00
|
|
|
|
|
|
|
|
|
val streamConnection = app_state.streamManager.getConnection(this)
|
2020-12-21 21:16:33 +01:00
|
|
|
|
?: return
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
|
|
|
|
val max = 40
|
|
|
|
|
val list = ArrayList<EntityId>(max * 2) // リブログなどで膨れる場合がある
|
|
|
|
|
|
|
|
|
|
fun add(s: TootStatus?) {
|
|
|
|
|
s ?: return
|
|
|
|
|
list.add(s.id)
|
|
|
|
|
add(s.reblog)
|
|
|
|
|
add(s.reply)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (i in 0 until min(max, list_data.size)) {
|
|
|
|
|
val o = list_data[i]
|
|
|
|
|
if (o is TootStatus) {
|
|
|
|
|
add(o)
|
|
|
|
|
} else if (o is TootNotification) {
|
|
|
|
|
add(o.status)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-21 03:13:03 +01:00
|
|
|
|
if (list.isNotEmpty()) streamConnection.misskeySetCapture(list)
|
2020-12-06 21:19:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 既存データ中の会話サマリ項目と追加データの中にIDが同じものがあれば
|
|
|
|
|
// 既存データを入れ替えて追加データから削除するか
|
|
|
|
|
// 既存データを削除するかする
|
|
|
|
|
internal fun replaceConversationSummary(
|
2020-12-21 03:13:03 +01:00
|
|
|
|
changeList: ArrayList<AdapterChange>,
|
|
|
|
|
list_new: ArrayList<TimelineItem>,
|
|
|
|
|
list_data: BucketList<TimelineItem>
|
|
|
|
|
) {
|
2020-12-06 21:19:02 +01:00
|
|
|
|
|
|
|
|
|
val newMap = HashMap<EntityId, TootConversationSummary>().apply {
|
|
|
|
|
for (o in list_new) {
|
|
|
|
|
if (o is TootConversationSummary) this[o.id] = o
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (list_data.isEmpty() || newMap.isEmpty()) return
|
|
|
|
|
|
|
|
|
|
val removeSet = HashSet<EntityId>()
|
|
|
|
|
for (i in list_data.size - 1 downTo 0) {
|
|
|
|
|
val o = list_data[i] as? TootConversationSummary ?: continue
|
|
|
|
|
val newItem = newMap[o.id] ?: continue
|
|
|
|
|
|
|
|
|
|
if (o.last_status.uri == newItem.last_status.uri) {
|
|
|
|
|
// 投稿が同じなので順序を入れ替えず、その場所で更新する
|
|
|
|
|
changeList.add(AdapterChange(AdapterChangeType.RangeChange, i, 1))
|
|
|
|
|
list_data[i] = newItem
|
|
|
|
|
removeSet.add(newItem.id)
|
|
|
|
|
log.d("replaceConversationSummary: in-place update")
|
|
|
|
|
} else {
|
|
|
|
|
// 投稿が異なるので古い方を削除して、リストの順序を変える
|
|
|
|
|
changeList.add(AdapterChange(AdapterChangeType.RangeRemove, i, 1))
|
|
|
|
|
list_data.removeAt(i)
|
|
|
|
|
log.d("replaceConversationSummary: order change")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val it = list_new.iterator()
|
|
|
|
|
while (it.hasNext()) {
|
|
|
|
|
val o = it.next() as? TootConversationSummary ?: continue
|
|
|
|
|
if (removeSet.contains(o.id)) it.remove()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// フィルタを読み直してリストを返す。またはnull
|
2020-12-08 21:24:42 +01:00
|
|
|
|
internal suspend fun loadFilter2(client: TootApiClient): ArrayList<TootFilter>? {
|
2020-12-06 21:19:02 +01:00
|
|
|
|
if (access_info.isPseudo || access_info.isMisskey) return null
|
|
|
|
|
val column_context = getFilterContext()
|
|
|
|
|
if (column_context == 0) return null
|
|
|
|
|
val result = client.request(PATH_FILTERS)
|
|
|
|
|
|
|
|
|
|
val jsonArray = result?.jsonArray ?: return null
|
|
|
|
|
return TootFilter.parseList(jsonArray)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal fun 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) {
|
2020-12-21 03:13:03 +01:00
|
|
|
|
true -> WordTrieTree.WORD_VALIDATOR
|
2020-12-06 21:19:02 +01:00
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal fun checkFiltersForListData(trees: FilterTrees?) {
|
|
|
|
|
trees ?: return
|
|
|
|
|
|
|
|
|
|
val changeList = ArrayList<AdapterChange>()
|
|
|
|
|
list_data.forEachIndexed { idx, item ->
|
|
|
|
|
when (item) {
|
2020-12-21 03:13:03 +01:00
|
|
|
|
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))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-12-06 21:19:02 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fireShowContent(reason = "filter updated", changeList = changeList)
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun onFiltersChanged2(filterList: ArrayList<TootFilter>) {
|
|
|
|
|
val newFilter = encodeFilterTree(filterList) ?: return
|
|
|
|
|
this.keywordFilterTrees = newFilter
|
|
|
|
|
checkFiltersForListData(newFilter)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun 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 onScheduleDeleted(item: TootScheduled) {
|
|
|
|
|
val tmp_list = ArrayList<TimelineItem>(list_data.size)
|
|
|
|
|
for (o in list_data) {
|
|
|
|
|
if (o === item) continue
|
|
|
|
|
tmp_list.add(o)
|
|
|
|
|
}
|
|
|
|
|
if (tmp_list.size != list_data.size) {
|
|
|
|
|
list_data.clear()
|
|
|
|
|
list_data.addAll(tmp_list)
|
|
|
|
|
fireShowContent(reason = "onScheduleDeleted")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fun 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) {
|
|
|
|
|
log.e(ex, "can't get last_viewing_item_id.")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun getContentColor(): Int = when {
|
|
|
|
|
content_color != 0 -> content_color
|
|
|
|
|
else -> defaultColorContentText
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun getAcctColor(): Int = when {
|
|
|
|
|
acct_color != 0 -> acct_color
|
|
|
|
|
else -> defaultColorContentAcct
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun getHeaderPageNumberColor() = when {
|
|
|
|
|
header_fg_color != 0 -> header_fg_color
|
|
|
|
|
else -> defaultColorHeaderPageNumber
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun getHeaderNameColor() = when {
|
|
|
|
|
header_fg_color != 0 -> header_fg_color
|
|
|
|
|
else -> defaultColorHeaderName
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun getHeaderBackgroundColor() = when {
|
|
|
|
|
header_bg_color != 0 -> header_bg_color
|
|
|
|
|
else -> defaultColorHeaderBg
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun setHeaderBackground(view: View) {
|
|
|
|
|
view.backgroundDrawable = getAdaptiveRippleDrawable(
|
2020-12-21 03:13:03 +01:00
|
|
|
|
getHeaderBackgroundColor(),
|
|
|
|
|
getHeaderNameColor()
|
|
|
|
|
)
|
2020-12-06 21:19:02 +01:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-17 07:03:18 +02:00
|
|
|
|
// Fedibird 絵文字リアクション機能
|
|
|
|
|
// APIの戻り値や通知データに新しいステータス情報が含まれるので、カラム中の該当する投稿のリアクション情報を更新する
|
|
|
|
|
fun updateEmojiReaction(newStatus: TootStatus?) {
|
|
|
|
|
newStatus ?: return
|
|
|
|
|
val statusId = newStatus.id
|
|
|
|
|
val newReactionSet = newStatus.reactionSet ?: TootReactionSet(isMisskey = false)
|
|
|
|
|
|
|
|
|
|
val changeList = ArrayList<AdapterChange>()
|
|
|
|
|
|
|
|
|
|
fun scanStatus1(s: TootStatus?, idx: Int, block: (s: TootStatus) -> Boolean) {
|
|
|
|
|
s ?: return
|
|
|
|
|
if (s.id == statusId) {
|
|
|
|
|
if (block(s)) {
|
|
|
|
|
changeList.add(AdapterChange(AdapterChangeType.RangeChange, idx, 1))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
scanStatus1(s.reblog, idx, block)
|
|
|
|
|
scanStatus1(s.reply, idx, block)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun scanStatusAll(block: (s: TootStatus) -> Boolean) {
|
|
|
|
|
for (i in 0 until list_data.size) {
|
|
|
|
|
val o = list_data[i]
|
|
|
|
|
if (o is TootStatus) {
|
|
|
|
|
scanStatus1(o, i, block)
|
|
|
|
|
} else if (o is TootNotification) {
|
|
|
|
|
scanStatus1(o.status, i, block)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
scanStatusAll { s ->
|
|
|
|
|
s.updateReactionMastodon(newReactionSet)
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (changeList.isNotEmpty()) {
|
|
|
|
|
fireShowContent(reason = "onEmojiReaction", changeList = changeList)
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-12-21 03:13:03 +01:00
|
|
|
|
|
2020-12-06 21:19:02 +01:00
|
|
|
|
// fun findListIndexByTimelineId(orderId : EntityId) : Int? {
|
|
|
|
|
// list_data.forEachIndexed { i, v ->
|
|
|
|
|
// if(v.getOrderId() == orderId) return i
|
|
|
|
|
// }
|
|
|
|
|
// return null
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
init {
|
|
|
|
|
registerColumnId(column_id, this)
|
|
|
|
|
}
|
2021-05-17 07:03:18 +02:00
|
|
|
|
|
|
|
|
|
|
2018-01-04 19:52:25 +01:00
|
|
|
|
}
|