Compare commits

...

5 Commits

41 changed files with 657 additions and 391 deletions

81
.gitignore vendored
View File

@ -1,3 +1,84 @@
#####################################
# https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
##################################### #####################################
# https://github.com/github/gitignore/blob/master/Android.gitignore # https://github.com/github/gitignore/blob/master/Android.gitignore
# Built application files # Built application files

10
.idea/migrations.xml Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

View File

@ -164,7 +164,7 @@ dependencies {
implementation(project(":anko")) implementation(project(":anko"))
implementation(fileTree(mapOf("dir" to "src/main/libs", "include" to arrayOf("*.aar")))) implementation(fileTree(mapOf("dir" to "src/main/libs", "include" to arrayOf("*.aar"))))
"fcmImplementation"("com.google.firebase:firebase-messaging:23.3.1") "fcmImplementation"("com.google.firebase:firebase-messaging:23.4.0")
"fcmImplementation"("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:${Vers.kotlinxCoroutinesVersion}") "fcmImplementation"("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:${Vers.kotlinxCoroutinesVersion}")
// implementation "org.conscrypt:conscrypt-android:$conscryptVersion" // implementation "org.conscrypt:conscrypt-android:$conscryptVersion"
@ -272,25 +272,26 @@ tasks.register<Detekt>("detektAll") {
) )
) )
// val kotlinFiles = "**/*.kt"
// include(kotlinFiles)
val resourceFiles = "**/resources/**" val resourceFiles = "**/resources/**"
val buildFiles = "**/build/**" val buildFiles = "**/build/**"
exclude(resourceFiles, buildFiles) exclude(resourceFiles, buildFiles)
reports { reports {
val buildDir = layout.buildDirectory fun reportLocationByExt(ext: String) =
layout.buildDirectory
xml.required.set(false) .file("reports/detekt/st-${name}.$ext")
xml.outputLocation.set(file("$buildDir/reports/detekt/st-${name}.xml")) .get()
.asFile
html.required.set(true)
html.outputLocation.set(file("$buildDir/reports/detekt/st-${name}.html"))
txt.required.set(true) txt.required.set(true)
txt.outputLocation.set(file("$buildDir/reports/detekt/st-${name}.txt")) txt.outputLocation.set(reportLocationByExt("txt"))
sarif.required.set(true) html.required.set(true)
sarif.outputLocation.set(file("$buildDir/reports/detekt/st-${name}.sarif")) html.outputLocation.set(reportLocationByExt("html"))
xml.required.set(false)
xml.outputLocation.set(reportLocationByExt("xml"))
sarif.required.set(false)
sarif.outputLocation.set(reportLocationByExt("sarif"))
} }
} }

View File

@ -19,7 +19,13 @@ import android.view.View.FOCUS_FORWARD
import android.view.ViewGroup import android.view.ViewGroup
import android.view.Window import android.view.Window
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.* import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.BaseAdapter
import android.widget.CompoundButton
import android.widget.FrameLayout
import android.widget.Spinner
import android.widget.TextView
import android.widget.TextView.OnEditorActionListener import android.widget.TextView.OnEditorActionListener
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
@ -53,22 +59,35 @@ import jp.juggler.subwaytooter.util.CustomShare
import jp.juggler.subwaytooter.util.CustomShareTarget import jp.juggler.subwaytooter.util.CustomShareTarget
import jp.juggler.subwaytooter.util.cn import jp.juggler.subwaytooter.util.cn
import jp.juggler.subwaytooter.view.MyTextView import jp.juggler.subwaytooter.view.MyTextView
import jp.juggler.util.* import jp.juggler.util.backPressed
import jp.juggler.util.coroutine.launchAndShowError import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchProgress import jp.juggler.util.coroutine.launchProgress
import jp.juggler.util.data.* import jp.juggler.util.data.cast
import jp.juggler.util.data.defaultLocale
import jp.juggler.util.data.handleGetContentResult
import jp.juggler.util.data.intentOpenDocument
import jp.juggler.util.data.notEmpty
import jp.juggler.util.data.notZero
import jp.juggler.util.getPackageInfoCompat
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.dialogOrToast import jp.juggler.util.log.dialogOrToast
import jp.juggler.util.log.showToast import jp.juggler.util.log.showToast
import jp.juggler.util.log.withCaption import jp.juggler.util.log.withCaption
import jp.juggler.util.ui.* import jp.juggler.util.queryIntentActivitiesCompat
import jp.juggler.util.ui.ActivityResultHandler
import jp.juggler.util.ui.attrColor
import jp.juggler.util.ui.hideKeyboard
import jp.juggler.util.ui.isEnabledAlpha
import jp.juggler.util.ui.isNotOk
import jp.juggler.util.ui.launch
import jp.juggler.util.ui.vg
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.InputStream import java.io.InputStream
import java.io.OutputStreamWriter import java.io.OutputStreamWriter
import java.text.NumberFormat import java.text.NumberFormat
import java.util.* import java.util.TimeZone
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
@ -175,7 +194,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
if (savedInstanceState != null) { if (savedInstanceState != null) {
try { try {
savedInstanceState.getString(STATE_CHOOSE_INTENT_TARGET)?.let { target -> savedInstanceState.getString(STATE_CHOOSE_INTENT_TARGET)?.let { target ->
customShareTarget = CustomShareTarget.values().firstOrNull { it.name == target } customShareTarget = CustomShareTarget.entries.find { it.name == target }
} }
} catch (ex: Throwable) { } catch (ex: Throwable) {
log.e(ex, "can't restore customShareTarget.") log.e(ex, "can't restore customShareTarget.")

View File

@ -79,12 +79,14 @@ class ActCallback : AppCompatActivity() {
sharedIntent.set(intent) sharedIntent.set(intent)
} }
} }
forbidUriFromApp(intent) -> { forbidUriFromApp(intent) -> {
// last_uriをクリアする // last_uriをクリアする
lastUri.set(null) lastUri.set(null)
// ダイアログを閉じるまで画面遷移しない // ダイアログを閉じるまで画面遷移しない
return return
} }
else -> { else -> {
val uri = intent.data val uri = intent.data
if (uri != null) { if (uri != null) {
@ -123,7 +125,7 @@ class ActCallback : AppCompatActivity() {
val type = src.type val type = src.type
if (type.isMediaMimeType()) { if (type.isMediaMimeType()) {
when (action){ when (action) {
Intent.ACTION_VIEW -> { Intent.ACTION_VIEW -> {
src.data?.let { uriOriginal -> src.data?.let { uriOriginal ->
try { try {
@ -137,6 +139,7 @@ class ActCallback : AppCompatActivity() {
} }
} }
} }
Intent.ACTION_SEND -> { Intent.ACTION_SEND -> {
var uri = src.getStreamUriExtra() var uri = src.getStreamUriExtra()
?: return src // text/plainの場合 ?: return src // text/plainの場合
@ -152,7 +155,8 @@ class ActCallback : AppCompatActivity() {
log.e(ex, "remake failed. src=$src") log.e(ex, "remake failed. src=$src")
} }
} }
Intent.ACTION_SEND_MULTIPLE -> {
Intent.ACTION_SEND_MULTIPLE -> {
val listUri = src.getStreamUriListExtra() val listUri = src.getStreamUriListExtra()
?: return null ?: return null
val listDst = ArrayList<Uri>() val listDst = ArrayList<Uri>()

View File

@ -247,7 +247,7 @@ class ActKeywordFilter : AppCompatActivity() {
if (result?.response?.code == 404) { if (result?.response?.code == 404) {
// try v1 // try v1
result = client.request("${ApiPath.PATH_FILTERS}/$filterId") result = client.request("${ApiPath.PATH_FILTERS_V1}/$filterId")
result?.jsonObject?.let { result?.jsonObject?.let {
try { try {
resultFilter = TootFilter(it) resultFilter = TootFilter(it)
@ -391,12 +391,12 @@ class ActKeywordFilter : AppCompatActivity() {
return runApiTask(account) { client -> return runApiTask(account) { client ->
if (filterId == null) { if (filterId == null) {
client.request( client.request(
ApiPath.PATH_FILTERS, ApiPath.PATH_FILTERS_V1,
params.toPostRequestBuilder() params.toPostRequestBuilder()
) )
} else { } else {
client.request( client.request(
"${ApiPath.PATH_FILTERS}/$filterId", "${ApiPath.PATH_FILTERS_V1}/$filterId",
params.toRequestBody().toPut() params.toRequestBody().toPut()
) )
} }

View File

@ -7,11 +7,17 @@ import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.actmain.addColumn import jp.juggler.subwaytooter.actmain.addColumn
import jp.juggler.subwaytooter.actmain.reloadAccountSetting import jp.juggler.subwaytooter.actmain.reloadAccountSetting
import jp.juggler.subwaytooter.actmain.showColumnMatchAccount import jp.juggler.subwaytooter.actmain.showColumnMatchAccount
import jp.juggler.subwaytooter.api.* import jp.juggler.subwaytooter.api.ApiTask
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.Acct import jp.juggler.subwaytooter.api.entity.Acct
import jp.juggler.subwaytooter.api.entity.EntityId import jp.juggler.subwaytooter.api.entity.EntityId
import jp.juggler.subwaytooter.api.entity.TootStatus import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.api.entity.TootVisibility import jp.juggler.subwaytooter.api.entity.TootVisibility
import jp.juggler.subwaytooter.api.errorApiResult
import jp.juggler.subwaytooter.api.runApiTask2
import jp.juggler.subwaytooter.api.syncStatus
import jp.juggler.subwaytooter.column.ColumnType import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.column.findStatus import jp.juggler.subwaytooter.column.findStatus
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
@ -27,6 +33,7 @@ import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.JsonObject import jp.juggler.util.data.JsonObject
import jp.juggler.util.log.showToast import jp.juggler.util.log.showToast
import jp.juggler.util.network.toPostRequestBuilder import jp.juggler.util.network.toPostRequestBuilder
import kotlinx.coroutines.CancellationException
import kotlin.math.max import kotlin.math.max
private class BoostImpl( private class BoostImpl(
@ -47,8 +54,6 @@ private class BoostImpl(
private val isPrivateToot = accessInfo.isMastodon && private val isPrivateToot = accessInfo.isMastodon &&
statusArg.visibility == TootVisibility.PrivateFollowers statusArg.visibility == TootVisibility.PrivateFollowers
private var bConfirmed = false
private fun preCheck(): Boolean { private fun preCheck(): Boolean {
// アカウントからステータスにブースト操作を行っているなら、何もしない // アカウントからステータスにブースト操作を行っているなら、何もしない
@ -73,15 +78,15 @@ private class BoostImpl(
} else { } else {
val (result, status) = client.syncStatus(accessInfo, statusArg) val (result, status) = client.syncStatus(accessInfo, statusArg)
when { when {
result == null -> throw CancellationException()
status == null -> errorApiResult(result) status == null -> errorApiResult(result)
status.reblogged -> errorApiResult(getString(R.string.already_boosted)) status.reblogged -> errorApiResult(R.string.already_boosted)
else -> status else -> status
} }
} }
// ブースト結果をUIに反映させる // ブースト結果をUIに反映させる
private fun after(result: TootApiResult?, newStatus: TootStatus?, unrenoteId: EntityId?) { private fun after(newStatus: TootStatus?, unrenoteId: EntityId?) {
result ?: return // cancelled.
when { when {
// Misskeyでunrenoteに成功した // Misskeyでunrenoteに成功した
unrenoteId != null -> { unrenoteId != null -> {
@ -143,36 +148,20 @@ private class BoostImpl(
} }
callback() callback()
} }
else -> activity.showToast(true, result.error)
} }
} }
suspend fun boostApi(client: TootApiClient, targetStatus: TootStatus): TootApiResult? = suspend fun boostApi(client: TootApiClient, targetStatus: TootStatus): TootApiResult =
if (accessInfo.isMisskey) { when {
if (!bSet) { accessInfo.isMisskey -> when {
val myRenoteId = targetStatus.myRenoteId ?: errorApiResult("missing renote id.") // misskey, create renote
bSet -> client.requestOrThrow(
client.request(
"/api/notes/delete",
accessInfo.putMisskeyApiToken().apply {
put("noteId", myRenoteId.toString())
put("renoteId", targetStatus.id.toString())
}.toPostRequestBuilder()
)?.also {
if (it.response?.code == 204) {
resultUnrenoteId = myRenoteId
}
}
} else {
client.request(
"/api/notes/create", "/api/notes/create",
accessInfo.putMisskeyApiToken().apply { accessInfo.putMisskeyApiToken().apply {
put("renoteId", targetStatus.id.toString()) put("renoteId", targetStatus.id.toString())
}.toPostRequestBuilder() }.toPostRequestBuilder()
)?.also { result -> ).apply {
val jsonObject = result.jsonObject jsonObject?.let { jsonObject ->
if (jsonObject != null) {
val outerStatus = val outerStatus =
parser.status(jsonObject.jsonObject("createdNote") ?: jsonObject) parser.status(jsonObject.jsonObject("createdNote") ?: jsonObject)
val innerStatus = outerStatus?.reblog ?: outerStatus val innerStatus = outerStatus?.reblog ?: outerStatus
@ -184,73 +173,84 @@ private class BoostImpl(
resultStatus = innerStatus resultStatus = innerStatus
} }
} }
} // misskey, delete renote
} else { else -> {
val b = JsonObject().apply { val myRenoteId = targetStatus.myRenoteId
if (visibility != null) put("visibility", visibility.strMastodon) ?: error("missing renote id.")
}.toPostRequestBuilder()
client.request( client.requestOrThrow(
"/api/notes/delete",
accessInfo.putMisskeyApiToken().apply {
put("noteId", myRenoteId.toString())
put("renoteId", targetStatus.id.toString())
}.toPostRequestBuilder()
).apply {
if (response?.code == 204) {
resultUnrenoteId = myRenoteId
}
}
}
}
// mastodon, reblog or unreblog
else -> client.requestOrThrow(
"/api/v1/statuses/${targetStatus.id}/${if (bSet) "reblog" else "unreblog"}", "/api/v1/statuses/${targetStatus.id}/${if (bSet) "reblog" else "unreblog"}",
b requestBuilder = JsonObject().apply {
)?.also { result -> if (visibility != null) put("visibility", visibility.strMastodon)
}.toPostRequestBuilder()
).apply {
// reblogはreblogを表すStatusを返す // reblogはreblogを表すStatusを返す
// unreblogはreblogしたStatusを返す // unreblogはreblogしたStatusを返す
val s = parser.status(result.jsonObject) val s = parser.status(jsonObject)
resultStatus = s?.reblog ?: s resultStatus = s?.reblog ?: s
} }
} }
fun run() { fun run() = activity.launchAndShowError {
activity.launchAndShowError { if (!preCheck()) return@launchAndShowError
if (!preCheck()) return@launchAndShowError
if (!bConfirmed) { val confirmMessage = activity.getString(
activity.confirm( when {
activity.getString( !bSet -> R.string.confirm_unboost_from
when { isPrivateToot -> R.string.confirm_boost_private_from
!bSet -> R.string.confirm_unboost_from visibility == TootVisibility.PrivateFollowers ->
isPrivateToot -> R.string.confirm_boost_private_from R.string.confirm_private_boost_from
visibility == TootVisibility.PrivateFollowers -> R.string.confirm_private_boost_from
else -> R.string.confirm_boost_from else -> R.string.confirm_boost_from
}, },
daoAcctColor.getNickname(accessInfo) daoAcctColor.getNickname(accessInfo)
), )
when (bSet) {
true -> accessInfo.confirmBoost activity.confirm(
else -> accessInfo.confirmUnboost message = confirmMessage,
} isConfirmEnabled = when (bSet) {
) { newConfirmEnabled -> true -> accessInfo.confirmBoost
when (bSet) { else -> accessInfo.confirmUnboost
true -> accessInfo.confirmBoost = newConfirmEnabled
else -> accessInfo.confirmUnboost = newConfirmEnabled
}
daoSavedAccount.save(accessInfo)
activity.reloadAccountSetting(accessInfo)
}
} }
) { newConfirmEnabled ->
when (bSet) {
true -> accessInfo.confirmBoost = newConfirmEnabled
else -> accessInfo.confirmUnboost = newConfirmEnabled
}
daoSavedAccount.save(accessInfo)
activity.reloadAccountSetting(accessInfo)
}
// ブースト表示を更新中にする // ブースト表示を更新中にする
activity.appState.setBusyBoost(accessInfo, statusArg) activity.appState.setBusyBoost(accessInfo, statusArg)
activity.showColumnMatchAccount(accessInfo) activity.showColumnMatchAccount(accessInfo)
try {
val result = activity.runApiTask2(
activity.runApiTask( accessInfo = accessInfo,
accessInfo, progressStyle = ApiTask.PROGRESS_NONE
progressStyle = ApiTask.PROGRESS_NONE ) { client ->
) { client -> boostApi(client, syncStatus(client))
try { }
val targetStatus = syncStatus(client) // カラムデータの書き換え
boostApi(client, targetStatus) after(resultStatus, resultUnrenoteId)
} catch (ex: TootApiResultException) { } finally {
ex.result
}
}
// 更新中状態をリセット // 更新中状態をリセット
activity.appState.resetBusyBoost(accessInfo, statusArg) activity.appState.resetBusyBoost(accessInfo, statusArg)
// カラムデータの書き換え // 失敗やキャンセルの場合でも表示を更新する
after(result, resultStatus, resultUnrenoteId)
// result == null の場合でも更新中表示の解除が必要になる
activity.showColumnMatchAccount(accessInfo) activity.showColumnMatchAccount(accessInfo)
} }
} }

View File

@ -3,15 +3,19 @@ package jp.juggler.subwaytooter.action
import jp.juggler.subwaytooter.ActKeywordFilter import jp.juggler.subwaytooter.ActKeywordFilter
import jp.juggler.subwaytooter.ActMain import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.ApiPath
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.TootApiResultException
import jp.juggler.subwaytooter.api.entity.EntityId
import jp.juggler.subwaytooter.api.entity.TootFilter import jp.juggler.subwaytooter.api.entity.TootFilter
import jp.juggler.subwaytooter.api.runApiTask import jp.juggler.subwaytooter.api.runApiTask2
import jp.juggler.subwaytooter.column.onFilterDeleted import jp.juggler.subwaytooter.column.onFilterDeleted
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.dialog.actionsDialog import jp.juggler.subwaytooter.dialog.actionsDialog
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.coroutine.launchAndShowError import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.log.showToast import jp.juggler.util.log.showToast
import okhttp3.Request
// private val log = LogCategory("Action_Filter") // private val log = LogCategory("Action_Filter")
@ -30,39 +34,56 @@ fun ActMain.openFilterMenu(accessInfo: SavedAccount, item: TootFilter?) {
} }
} }
fun ActMain.filterDelete( suspend fun TootApiClient.filterDelete(filterId: EntityId): TootApiResult {
accessInfo: SavedAccount, for (path in arrayOf(
filter: TootFilter, "/api/v2/filters/${filterId}",
bConfirmed: Boolean = false, "/api/v1/filters/${filterId}",
) { )) {
launchAndShowError { try {
if (!bConfirmed) { return requestOrThrow(path = path)
confirm(R.string.filter_delete_confirm, filter.displayString) } catch (ex: TootApiResultException) {
} when (ex.result?.response?.code) {
404 -> continue
var resultFilterList: List<TootFilter>? = null else -> throw ex
runApiTask(accessInfo) { client ->
var result =
client.request("/api/v1/filters/${filter.id}", Request.Builder().delete())
if (result != null && result.error == null) {
result = client.request("/api/v1/filters")
val jsonArray = result?.jsonArray
if (jsonArray != null) resultFilterList = TootFilter.parseList(jsonArray)
}
result
}?.let { result ->
when (val filterList = resultFilterList) {
null -> showToast(false, result.error)
else -> {
showToast(false, R.string.delete_succeeded)
for (column in appState.columnList) {
if (column.accessInfo == accessInfo) {
column.onFilterDeleted(filter, filterList)
}
}
}
} }
} }
} }
error("missing filter APIs.")
}
suspend fun TootApiClient.filterLoad(): List<TootFilter> {
for (path in arrayOf(
ApiPath.PATH_FILTERS_V2,
ApiPath.PATH_FILTERS_V1,
)) {
try {
val jsonArray = requestOrThrow(path).jsonArray
?: error("API response has no jsonArray.")
return TootFilter.parseList(jsonArray)
?: error("TootFilter.parseList returns null.")
} catch (ex: TootApiResultException) {
when (ex.result?.response?.code) {
404 -> continue
else -> throw ex
}
}
}
error("missing filter APIs.")
}
fun ActMain.filterDelete(
accessInfo: SavedAccount,
filter: TootFilter,
) = launchAndShowError {
confirm(R.string.filter_delete_confirm, filter.displayString)
val newFilters = runApiTask2(accessInfo) { client ->
client.filterDelete(filter.id)
client.filterLoad()
}
showToast(false, R.string.delete_succeeded)
for (column in appState.columnList) {
if (column.accessInfo == accessInfo) {
column.onFilterDeleted(filter, newFilters)
}
}
} }

View File

@ -20,8 +20,24 @@ import android.widget.ListView
import android.widget.TextView import android.widget.TextView
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout import androidx.drawerlayout.widget.DrawerLayout
import jp.juggler.subwaytooter.* import jp.juggler.subwaytooter.ActAbout
import jp.juggler.subwaytooter.action.* import jp.juggler.subwaytooter.ActAppSetting
import jp.juggler.subwaytooter.ActFavMute
import jp.juggler.subwaytooter.ActHighlightWordList
import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.ActMutedApp
import jp.juggler.subwaytooter.ActMutedPseudoAccount
import jp.juggler.subwaytooter.ActMutedWord
import jp.juggler.subwaytooter.ActOSSLicense
import jp.juggler.subwaytooter.ActPushMessageList
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.action.accountAdd
import jp.juggler.subwaytooter.action.accountOpenSetting
import jp.juggler.subwaytooter.action.openColumnFromUrl
import jp.juggler.subwaytooter.action.openColumnList
import jp.juggler.subwaytooter.action.serverProfileDirectoryFromSideMenu
import jp.juggler.subwaytooter.action.timeline
import jp.juggler.subwaytooter.api.entity.TootStatus import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.column.ColumnType import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.dialog.pickAccount import jp.juggler.subwaytooter.dialog.pickAccount
@ -50,7 +66,7 @@ import jp.juggler.util.ui.createColoredDrawable
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jetbrains.anko.backgroundColor import org.jetbrains.anko.backgroundColor
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.* import java.util.TimeZone
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.math.abs import kotlin.math.abs
@ -71,7 +87,7 @@ class SideMenuAdapter(
private const val urlOlderDevices = private const val urlOlderDevices =
"https://github.com/tateisu/SubwayTooter/discussions/192" "https://github.com/tateisu/SubwayTooter/discussions/192"
private val itemTypeCount = ItemType.values().size private val itemTypeCount = ItemType.entries.size
private var lastVersionView: WeakReference<TextView>? = null private var lastVersionView: WeakReference<TextView>? = null
@ -487,10 +503,12 @@ class SideMenuAdapter(
when (itemType) { when (itemType) {
ItemType.IT_DIVIDER -> ItemType.IT_DIVIDER ->
viewOrInflate(view, parent, R.layout.lv_sidemenu_separator) viewOrInflate(view, parent, R.layout.lv_sidemenu_separator)
ItemType.IT_GROUP_HEADER -> ItemType.IT_GROUP_HEADER ->
viewOrInflate<TextView>(view, parent, R.layout.lv_sidemenu_group).apply { viewOrInflate<TextView>(view, parent, R.layout.lv_sidemenu_group).apply {
text = actMain.getString(title) text = actMain.getString(title)
} }
ItemType.IT_NORMAL -> ItemType.IT_NORMAL ->
viewOrInflate<TextView>(view, parent, R.layout.lv_sidemenu_item).apply { viewOrInflate<TextView>(view, parent, R.layout.lv_sidemenu_item).apply {
isAllCaps = false isAllCaps = false
@ -522,6 +540,7 @@ class SideMenuAdapter(
background = null background = null
text = versionText text = versionText
} }
ItemType.IT_TIMEZONE -> ItemType.IT_TIMEZONE ->
viewOrInflate<TextView>(view, parent, R.layout.lv_sidemenu_item).apply { viewOrInflate<TextView>(view, parent, R.layout.lv_sidemenu_item).apply {
textSize = 14f textSize = 14f
@ -529,6 +548,7 @@ class SideMenuAdapter(
background = null background = null
text = getTimeZoneString(context) text = getTimeZoneString(context)
} }
ItemType.IT_NOTIFICATION_PERMISSION -> ItemType.IT_NOTIFICATION_PERMISSION ->
viewOrInflate<TextView>(view, parent, R.layout.lv_sidemenu_item).apply { viewOrInflate<TextView>(view, parent, R.layout.lv_sidemenu_item).apply {
isAllCaps = false isAllCaps = false
@ -599,6 +619,7 @@ class SideMenuAdapter(
Pair(R.string.notification_push_distributor_disabled) { Pair(R.string.notification_push_distributor_disabled) {
actMain.selectPushDistributor() actMain.selectPushDistributor()
} }
else -> null else -> null
} }
@ -616,6 +637,7 @@ class SideMenuAdapter(
else -> true else -> true
} }
else -> true else -> true
} }
} }

View File

@ -41,7 +41,7 @@ object ApiPath {
// リストではなくオブジェクトを返すAPI // リストではなくオブジェクトを返すAPI
const val PATH_STATUSES = "/api/v1/statuses/%s" // 1:status_id const val PATH_STATUSES = "/api/v1/statuses/%s" // 1:status_id
const val PATH_FILTERS = "/api/v1/filters" const val PATH_FILTERS_V1 = "/api/v1/filters"
const val PATH_FILTERS_V2 = "/api/v2/filters" const val PATH_FILTERS_V2 = "/api/v2/filters"
const val PATH_MISSKEY_PROFILE_FOLLOWING = "/api/users/following" const val PATH_MISSKEY_PROFILE_FOLLOWING = "/api/users/following"

View File

@ -5,16 +5,42 @@ import android.net.Uri
import jp.juggler.subwaytooter.App1 import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.auth.AuthBase import jp.juggler.subwaytooter.api.auth.AuthBase
import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.entity.Acct
import jp.juggler.subwaytooter.api.entity.Host
import jp.juggler.subwaytooter.api.entity.TootAccount
import jp.juggler.subwaytooter.api.entity.TootAccountRef
import jp.juggler.subwaytooter.api.entity.TootAccountRef.Companion.tootAccountRefOrNull import jp.juggler.subwaytooter.api.entity.TootAccountRef.Companion.tootAccountRefOrNull
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.api.entity.TootResults
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.* import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.util.data.* import jp.juggler.subwaytooter.util.LinkHelper
import jp.juggler.subwaytooter.util.SimpleHttpClient
import jp.juggler.subwaytooter.util.SimpleHttpClientImpl
import jp.juggler.subwaytooter.util.matchHost
import jp.juggler.util.data.CharacterGroup
import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.asciiRegex
import jp.juggler.util.data.decodeJsonArray
import jp.juggler.util.data.decodeJsonObject
import jp.juggler.util.data.decodePercent
import jp.juggler.util.data.decodeUTF8
import jp.juggler.util.data.encodePercent
import jp.juggler.util.data.groupEx
import jp.juggler.util.data.letNotEmpty
import jp.juggler.util.data.notEmpty
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.withCaption import jp.juggler.util.log.withCaption
import jp.juggler.util.network.toPostRequestBuilder import jp.juggler.util.network.toPostRequestBuilder
import okhttp3.* import kotlinx.coroutines.CancellationException
import okhttp3.Call
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import okhttp3.internal.closeQuietly import okhttp3.internal.closeQuietly
class TootApiClient( class TootApiClient(
@ -358,7 +384,7 @@ class TootApiClient(
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// fun request( // fun request(
// path: String, // path: String,
// request_builder: Request.Builder = Request.Builder() // request_builder: Request.Builder = Request.Builder()
// ): TootApiResult? { // ): TootApiResult? {
@ -391,6 +417,26 @@ class TootApiClient(
// } // }
// //
/**
* requestと同じだがキャンセルやエラー発生時に例外を投げる
*/
suspend fun requestOrThrow(
path: String,
requestBuilder: Request.Builder = Request.Builder(),
forceAccessToken: String? = null,
): TootApiResult {
val result = request(
path = path,
requestBuilder = requestBuilder,
forceAccessToken = forceAccessToken,
)
when {
result == null -> throw CancellationException()
!result.error.isNullOrBlank() -> errorApiResult(result)
else -> return result
}
}
suspend fun request( suspend fun request(
path: String, path: String,
requestBuilder: Request.Builder = Request.Builder(), requestBuilder: Request.Builder = Request.Builder(),

View File

@ -1,12 +1,18 @@
package jp.juggler.subwaytooter.api package jp.juggler.subwaytooter.api
import android.content.Context
import androidx.annotation.StringRes
class TootApiResultException(val result: TootApiResult?) : class TootApiResultException(val result: TootApiResult?) :
Exception(result?.error ?: "cancelled.") { Exception(result?.error ?: "cancelled.") {
constructor(error: String) : this(TootApiResult(error)) constructor(error: String) : this(TootApiResult(error))
} }
fun errorApiResult(result: TootApiResult?): Nothing = fun errorApiResult(result: TootApiResult): Nothing =
throw TootApiResultException(result) throw TootApiResultException(result)
fun errorApiResult(error: String): Nothing = fun errorApiResult(error: String): Nothing =
throw TootApiResultException(error) throw TootApiResultException(error)
fun Context.errorApiResult(@StringRes stringId: Int, vararg args: Any?): Nothing =
errorApiResult(getString(stringId, *args))

View File

@ -2,7 +2,13 @@ package jp.juggler.subwaytooter.api.entity
import jp.juggler.subwaytooter.api.TootParser import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.util.data.* import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.addTo
import jp.juggler.util.data.buildJsonObject
import jp.juggler.util.data.clip
import jp.juggler.util.data.mayUri
import jp.juggler.util.data.notBlank
import jp.juggler.util.data.notEmpty
@Suppress("LongParameterList") @Suppress("LongParameterList")
class TootAttachment private constructor( class TootAttachment private constructor(
@ -92,7 +98,7 @@ class TootAttachment private constructor(
private val ext_audio = arrayOf(".mpga", ".mp3", ".aac", ".ogg") private val ext_audio = arrayOf(".mpga", ".mp3", ".aac", ".ogg")
private fun parseType(src: String?) = private fun parseType(src: String?) =
TootAttachmentType.values().find { it.id == src } TootAttachmentType.entries.find { it.id == src }
private fun guessMediaTypeByUrl(src: String?): TootAttachmentType? { private fun guessMediaTypeByUrl(src: String?): TootAttachmentType? {
val uri = src.mayUri() ?: return null val uri = src.mayUri() ?: return null
@ -104,6 +110,9 @@ class TootAttachment private constructor(
return null return null
} }
/**
* アプリ内でencodeJson()した情報をデコードする
*/
fun tootAttachmentJson( fun tootAttachmentJson(
src: JsonObject, src: JsonObject,
): TootAttachment { ): TootAttachment {
@ -147,8 +156,9 @@ class TootAttachment private constructor(
else -> TootAttachmentType.Unknown else -> TootAttachmentType.Unknown
} }
val url = src.string("url") val url = src.string("url")
val description = src.string("comment")?.notBlank() val description = (src.string("comment")?.notBlank()
?: src.string("name")?.notBlank() ?: src.string("name")?.notBlank())
?.takeIf { it != "null" }
return TootAttachment( return TootAttachment(
blurhash = null, blurhash = null,
description = description, description = description,
@ -189,7 +199,7 @@ class TootAttachment private constructor(
return TootAttachment( return TootAttachment(
blurhash = src.string("blurhash"), blurhash = src.string("blurhash"),
description = src.string("name"), description = src.string("name")?.notBlank()?.takeIf { it != "null" },
focusX = parseFocusValue(focus, "x"), focusX = parseFocusValue(focus, "x"),
focusY = parseFocusValue(focus, "y"), focusY = parseFocusValue(focus, "y"),
id = EntityId.DEFAULT, id = EntityId.DEFAULT,
@ -219,7 +229,8 @@ class TootAttachment private constructor(
return TootAttachment( return TootAttachment(
blurhash = src.string("blurhash"), blurhash = src.string("blurhash"),
description = src.string("description"), description = src.string("description")
?.notBlank()?.takeIf { it != "null" },
focusX = parseFocusValue(focus, "x"), focusX = parseFocusValue(focus, "x"),
focusY = parseFocusValue(focus, "y"), focusY = parseFocusValue(focus, "y"),
id = EntityId.mayDefault(src.string("id")), id = EntityId.mayDefault(src.string("id")),

View File

@ -23,13 +23,12 @@ enum class TootFilterContext(
companion object { companion object {
private val log = LogCategory("TootFilterContext") private val log = LogCategory("TootFilterContext")
private val valuesCache = values() private val apiNameMap = entries.associateBy { it.apiName }
private val apiNameMap = valuesCache.associateBy { it.apiName }
fun parseBits(src: JsonArray?): Int = fun parseBits(src: JsonArray?): Int =
src?.stringList()?.mapNotNull { apiNameMap[it]?.bit }?.sum() ?: 0 src?.stringList()?.mapNotNull { apiNameMap[it]?.bit }?.sum() ?: 0
fun bitsToNames(mask: Int) = fun bitsToNames(mask: Int) =
valuesCache.filter { it.bit.and(mask) != 0 }.map { it.caption_id } entries.filter { it.bit.and(mask) != 0 }.map { it.caption_id }
} }
} }

View File

@ -68,6 +68,7 @@ enum class TootVisibility(
LocalPublic, LocalHome -> true LocalPublic, LocalHome -> true
else -> false else -> false
} }
else -> when (this) { else -> when (this) {
Public, UnlistedHome -> true Public, UnlistedHome -> true
else -> false else -> false
@ -109,47 +110,35 @@ enum class TootVisibility(
companion object { companion object {
private val log = LogCategory("TootVisivbility") private val log = LogCategory("TootVisivbility")
fun parseMastodon(a: String?): TootVisibility? { fun parseMastodon(a: String?) =
for (v in values()) { entries.find { it.strMastodon == a }
if (v.strMastodon == a) return v
}
return null
}
fun parseMisskey(a: String?, localOnly: Boolean = false): TootVisibility? { fun parseMisskey(a: String?, localOnly: Boolean = false): TootVisibility? {
for (v in values()) { entries.find { it.strMisskey == a }?.let { v ->
if (v.strMisskey == a) { if (localOnly) {
if (localOnly) { when (v) {
when (v) { Public -> return LocalPublic
Public -> return LocalPublic UnlistedHome -> return LocalHome
UnlistedHome -> return LocalHome PrivateFollowers -> return LocalFollowers
PrivateFollowers -> return LocalFollowers
else -> { else -> Unit
}
}
} }
return v
} }
return v
} }
return null return null
} }
fun fromId(id: Int): TootVisibility? { fun fromId(id: Int) = entries.find { it.id == id }
for (v in values()) {
if (v.id == id) return v
}
return null
}
fun parseSavedVisibility(sv: String?): TootVisibility? { fun parseSavedVisibility(sv: String?): TootVisibility? {
sv ?: return null sv ?: return null
// 新しい方式ではenumのidの文字列表現 // 新しい方式ではenumのidの文字列表現
values().find { it.id.toString() == sv }?.let { return it } entries.find { it.id.toString() == sv }?.let { return it }
// 古い方式ではマストドンの公開範囲文字列かweb_setting // 古い方式ではマストドンの公開範囲文字列かweb_setting
values().find { it.strMastodon == sv }?.let { return it } entries.find { it.strMastodon == sv }?.let { return it }
return null return null
} }

View File

@ -70,7 +70,7 @@ enum class SettingType(val id: Int) {
; ;
companion object { companion object {
val map = values().associateBy { it.id } val map = entries.associateBy { it.id }
} }
} }
@ -367,7 +367,7 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
spinnerSimple( spinnerSimple(
PrefI.ipAdditionalButtonsPosition, PrefI.ipAdditionalButtonsPosition,
R.string.additional_buttons_position, R.string.additional_buttons_position,
*(AdditionalButtonsPosition.values().sortedBy { it.idx }.map { it.captionId } *(AdditionalButtonsPosition.entries.sortedBy { it.idx }.map { it.captionId }
.toIntArray()) .toIntArray())
) )
@ -380,7 +380,7 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
} }
section(R.string.translate_or_custom_share) { section(R.string.translate_or_custom_share) {
CustomShareTarget.values().forEach { target -> for (target in CustomShareTarget.entries) {
item( item(
SettingType.TextWithSelector, SettingType.TextWithSelector,
target.pref, target.pref,
@ -498,7 +498,7 @@ val appSettingRoot = AppSettingItem(null, SettingType.Section, R.string.app_sett
sw(PrefB.bpUseInternalMediaViewer, R.string.use_internal_media_viewer) sw(PrefB.bpUseInternalMediaViewer, R.string.use_internal_media_viewer)
spinner(PrefI.ipMediaBackground, R.string.background_pattern) { spinner(PrefI.ipMediaBackground, R.string.background_pattern) {
MediaBackgroundDrawable.Kind.values() MediaBackgroundDrawable.Kind.entries
.filter { it.isMediaBackground } .filter { it.isMediaBackground }
.map { it.name } .map { it.name }
} }

View File

@ -326,7 +326,7 @@ object ColumnEncoder {
-> { -> {
profileId = EntityId.mayNull(src.string(KEY_PROFILE_ID)) profileId = EntityId.mayNull(src.string(KEY_PROFILE_ID))
val tabId = src.optInt(KEY_PROFILE_TAB) val tabId = src.optInt(KEY_PROFILE_TAB)
profileTab = ProfileTab.values().find { it.id == tabId } ?: ProfileTab.Status profileTab = ProfileTab.entries.find { it.id == tabId } ?: ProfileTab.Status
} }
ColumnType.LIST_MEMBER, ColumnType.LIST_MEMBER,

View File

@ -8,7 +8,6 @@ import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.runApiTask import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.table.* import jp.juggler.subwaytooter.table.*
import jp.juggler.util.*
import jp.juggler.util.coroutine.launchMain import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.WordTrieTree import jp.juggler.util.data.WordTrieTree
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
@ -433,7 +432,7 @@ suspend fun Column.loadFilter2(client: TootApiClient): List<TootFilter>? {
if (getFilterContext() == null) return null if (getFilterContext() == null) return null
var result = client.request(ApiPath.PATH_FILTERS_V2) var result = client.request(ApiPath.PATH_FILTERS_V2)
if (result?.response?.code == 404) { if (result?.response?.code == 404) {
result = client.request(ApiPath.PATH_FILTERS) result = client.request(ApiPath.PATH_FILTERS_V1)
} }
val jsonArray = result?.jsonArray ?: return null val jsonArray = result?.jsonArray ?: return null
@ -511,7 +510,7 @@ fun reloadFilter(context: Context, accessInfo: SavedAccount) {
) { client -> ) { client ->
var result = client.request(ApiPath.PATH_FILTERS_V2) var result = client.request(ApiPath.PATH_FILTERS_V2)
if (result?.response?.code == 404) { if (result?.response?.code == 404) {
result = client.request(ApiPath.PATH_FILTERS) result = client.request(ApiPath.PATH_FILTERS_V1)
} }
result?.jsonArray?.let { result?.jsonArray?.let {
resultList = TootFilter.parseList(it) resultList = TootFilter.parseList(it)

View File

@ -10,7 +10,6 @@ import jp.juggler.subwaytooter.columnviewholder.scrollToTop
import jp.juggler.subwaytooter.notification.injectData import jp.juggler.subwaytooter.notification.injectData
import jp.juggler.subwaytooter.pref.PrefB import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.util.OpenSticker import jp.juggler.subwaytooter.util.OpenSticker
import jp.juggler.util.*
import jp.juggler.util.coroutine.runOnMainLooper import jp.juggler.util.coroutine.runOnMainLooper
import jp.juggler.util.coroutine.runOnMainLooperDelayed import jp.juggler.util.coroutine.runOnMainLooperDelayed
import jp.juggler.util.data.JsonArray import jp.juggler.util.data.JsonArray
@ -902,7 +901,7 @@ class ColumnTask_Loading(
suspend fun getFilterList(client: TootApiClient): TootApiResult? { suspend fun getFilterList(client: TootApiClient): TootApiResult? {
var result = client.request(ApiPath.PATH_FILTERS_V2) var result = client.request(ApiPath.PATH_FILTERS_V2)
if (result?.response?.code == 404) { if (result?.response?.code == 404) {
result = client.request(ApiPath.PATH_FILTERS) result = client.request(ApiPath.PATH_FILTERS_V1)
} }
if (result != null) { if (result != null) {
val src = TootFilter.parseList(result.jsonArray) val src = TootFilter.parseList(result.jsonArray)

View File

@ -6,9 +6,22 @@ import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.ApiPath import jp.juggler.subwaytooter.api.ApiPath
import jp.juggler.subwaytooter.api.TootApiClient import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootApiResult import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.entity.Acct
import jp.juggler.subwaytooter.api.entity.Host
import jp.juggler.subwaytooter.api.entity.TimelineItem
import jp.juggler.subwaytooter.api.entity.TootAccountRef.Companion.tootAccountRef import jp.juggler.subwaytooter.api.entity.TootAccountRef.Companion.tootAccountRef
import jp.juggler.subwaytooter.api.finder.* import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.api.entity.TootMessageHolder
import jp.juggler.subwaytooter.api.entity.TootNotification
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.api.finder.mastodonFollowSuggestion2ListParser
import jp.juggler.subwaytooter.api.finder.misskey11FollowersParser
import jp.juggler.subwaytooter.api.finder.misskey11FollowingParser
import jp.juggler.subwaytooter.api.finder.misskeyArrayFinderUsers
import jp.juggler.subwaytooter.api.finder.misskeyCustomParserBlocks
import jp.juggler.subwaytooter.api.finder.misskeyCustomParserFavorites
import jp.juggler.subwaytooter.api.finder.misskeyCustomParserFollowRequest
import jp.juggler.subwaytooter.api.finder.misskeyCustomParserMutes
import jp.juggler.subwaytooter.search.MspHelper.loadingMSP import jp.juggler.subwaytooter.search.MspHelper.loadingMSP
import jp.juggler.subwaytooter.search.MspHelper.refreshMSP import jp.juggler.subwaytooter.search.MspHelper.refreshMSP
import jp.juggler.subwaytooter.search.NotestockHelper.loadingNotestock import jp.juggler.subwaytooter.search.NotestockHelper.loadingNotestock
@ -17,10 +30,16 @@ import jp.juggler.subwaytooter.search.TootsearchHelper.loadingTootsearch
import jp.juggler.subwaytooter.search.TootsearchHelper.refreshTootsearch import jp.juggler.subwaytooter.search.TootsearchHelper.refreshTootsearch
import jp.juggler.subwaytooter.streaming.StreamSpec import jp.juggler.subwaytooter.streaming.StreamSpec
import jp.juggler.subwaytooter.table.daoAcctColor import jp.juggler.subwaytooter.table.daoAcctColor
import jp.juggler.util.* import jp.juggler.util.data.JsonArray
import jp.juggler.util.data.* import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.appendIf
import jp.juggler.util.data.ellipsizeDot3
import jp.juggler.util.data.jsonArrayOf
import jp.juggler.util.data.jsonObjectOf
import jp.juggler.util.data.notEmpty
import jp.juggler.util.data.toJsonArray
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import java.util.* import java.util.Locale
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@ -1128,6 +1147,7 @@ enum class ColumnType(
arrayFinder = misskeyArrayFinderUsers, arrayFinder = misskeyArrayFinderUsers,
listParser = misskeyCustomParserMutes listParser = misskeyCustomParserMutes
) )
else -> getAccountList(client, ApiPath.PATH_MUTES) else -> getAccountList(client, ApiPath.PATH_MUTES)
} }
}, },
@ -1142,6 +1162,7 @@ enum class ColumnType(
arrayFinder = misskeyArrayFinderUsers, arrayFinder = misskeyArrayFinderUsers,
listParser = misskeyCustomParserMutes listParser = misskeyCustomParserMutes
) )
else -> getAccountList( else -> getAccountList(
client, client,
ApiPath.PATH_MUTES, ApiPath.PATH_MUTES,
@ -1750,6 +1771,7 @@ enum class ColumnType(
ApiPath.PATH_FOLLOW_SUGGESTION2, ApiPath.PATH_FOLLOW_SUGGESTION2,
listParser = mastodonFollowSuggestion2ListParser, listParser = mastodonFollowSuggestion2ListParser,
) )
else -> else ->
getAccountList(client, ApiPath.PATH_FOLLOW_SUGGESTION) getAccountList(client, ApiPath.PATH_FOLLOW_SUGGESTION)
} }
@ -1773,6 +1795,7 @@ enum class ColumnType(
ApiPath.PATH_FOLLOW_SUGGESTION2, ApiPath.PATH_FOLLOW_SUGGESTION2,
listParser = mastodonFollowSuggestion2ListParser, listParser = mastodonFollowSuggestion2ListParser,
) )
else -> else ->
getAccountList(client, ApiPath.PATH_FOLLOW_SUGGESTION) getAccountList(client, ApiPath.PATH_FOLLOW_SUGGESTION)
} }
@ -1798,6 +1821,7 @@ enum class ColumnType(
listParser = mastodonFollowSuggestion2ListParser, listParser = mastodonFollowSuggestion2ListParser,
mastodonFilterByIdRange = false mastodonFilterByIdRange = false
) )
else -> else ->
getAccountList( getAccountList(
client, client,
@ -2088,7 +2112,7 @@ enum class ColumnType(
fun dump() { fun dump() {
var min = Int.MAX_VALUE var min = Int.MAX_VALUE
var max = Int.MIN_VALUE var max = Int.MIN_VALUE
values().forEach { for (it in entries) {
val id = it.id val id = it.id
min = min(min, id) min = min(min, id)
max = max(max, id) max = max(max, id)

View File

@ -3,7 +3,9 @@ package jp.juggler.subwaytooter.dialog
import android.app.Dialog import android.app.Dialog
import android.view.WindowManager import android.view.WindowManager
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.widget.* import android.widget.ArrayAdapter
import android.widget.Filter
import android.widget.TextView
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
@ -21,15 +23,23 @@ import jp.juggler.util.coroutine.launchAndShowError
import jp.juggler.util.coroutine.launchMain import jp.juggler.util.coroutine.launchMain
import jp.juggler.util.data.notBlank import jp.juggler.util.data.notBlank
import jp.juggler.util.data.notEmpty import jp.juggler.util.data.notEmpty
import jp.juggler.util.log.* import jp.juggler.util.log.LogCategory
import jp.juggler.util.ui.* import jp.juggler.util.log.showToast
import jp.juggler.util.ui.ProgressDialogEx
import jp.juggler.util.ui.attrColor
import jp.juggler.util.ui.dismissSafe
import jp.juggler.util.ui.hideKeyboard
import jp.juggler.util.ui.invisible
import jp.juggler.util.ui.isEnabledAlpha
import jp.juggler.util.ui.vg
import jp.juggler.util.ui.visible
import jp.juggler.util.ui.visibleOrInvisible
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jetbrains.anko.textColor import org.jetbrains.anko.textColor
import org.jetbrains.anko.textResource import org.jetbrains.anko.textResource
import java.io.BufferedReader import java.io.BufferedReader
import java.io.InputStreamReader import java.io.InputStreamReader
import java.net.IDN import java.net.IDN
import java.util.*
class LoginForm( class LoginForm(
val activity: AppCompatActivity, val activity: AppCompatActivity,
@ -62,7 +72,8 @@ class LoginForm(
) { ) {
Login(R.string.existing_account, R.string.existing_account_desc), Login(R.string.existing_account, R.string.existing_account_desc),
Pseudo(R.string.pseudo_account, R.string.pseudo_account_desc), Pseudo(R.string.pseudo_account, R.string.pseudo_account_desc),
// Create(2, R.string.create_account, R.string.create_account_desc),
// Create(2, R.string.create_account, R.string.create_account_desc),
Token(R.string.input_access_token, R.string.input_access_token_desc), Token(R.string.input_access_token, R.string.input_access_token_desc),
} }
@ -76,7 +87,7 @@ class LoginForm(
private var targetServerInfo: TootInstance? = null private var targetServerInfo: TootInstance? = null
init { init {
for (a in Action.values()) { for (a in Action.entries) {
val subViews = val subViews =
LvAuthTypeBinding.inflate(activity.layoutInflater, views.llPageAuthType, true) LvAuthTypeBinding.inflate(activity.layoutInflater, views.llPageAuthType, true)
subViews.btnAuthType.textResource = a.idName subViews.btnAuthType.textResource = a.idName

View File

@ -1,10 +1,15 @@
package jp.juggler.subwaytooter.drawable package jp.juggler.subwaytooter.drawable
import android.content.Context import android.content.Context
import android.graphics.* import android.graphics.Canvas
import android.graphics.Color
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.Rect
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.util.ui.* import jp.juggler.util.ui.attrColor
import kotlin.math.min import kotlin.math.min
class MediaBackgroundDrawable( class MediaBackgroundDrawable(
@ -33,10 +38,10 @@ class MediaBackgroundDrawable(
; ;
fun toIndex() = values().indexOf(this) fun toIndex() = entries.indexOf(this)
companion object { companion object {
fun fromIndex(idx: Int) = values().elementAtOrNull(idx) ?: BlackTile fun fromIndex(idx: Int) = entries.elementAtOrNull(idx) ?: BlackTile
} }
} }

View File

@ -20,9 +20,7 @@ class EmojiMapLoader(
private val assetsSet = appContext.assets.list("")!!.toSet() private val assetsSet = appContext.assets.list("")!!.toSet()
private val resources = appContext.resources!! private val resources = appContext.resources!!
private val categoryNameMap = HashMap<String, EmojiCategory>().apply { private val categoryNameMap = EmojiCategory.entries.associateBy { it.name }
EmojiCategory.values().forEach { put(it.name, it) }
}
private var lastEmoji: UnicodeEmoji? = null private var lastEmoji: UnicodeEmoji? = null
private var lastCategory: EmojiCategory? = null private var lastCategory: EmojiCategory? = null
@ -60,28 +58,34 @@ class EmojiMapLoader(
if (!assetsSet.contains(line)) error("missing assets.") if (!assetsSet.contains(line)) error("missing assets.")
lastEmoji = UnicodeEmoji(assetsName = line) lastEmoji = UnicodeEmoji(assetsName = line)
} }
"drawable" -> { "drawable" -> {
val drawableId = getDrawableId(line) ?: error("missing drawable.") val drawableId = getDrawableId(line) ?: error("missing drawable.")
lastEmoji = UnicodeEmoji(drawableId = drawableId) lastEmoji = UnicodeEmoji(drawableId = drawableId)
} }
"un" -> { "un" -> {
val emoji = lastEmoji ?: error("missing lastEmoji.") val emoji = lastEmoji ?: error("missing lastEmoji.")
addCode(emoji, line) addCode(emoji, line)
emoji.unifiedCode = line emoji.unifiedCode = line
} }
"u" -> { "u" -> {
val emoji = lastEmoji ?: error("missing lastEmoji.") val emoji = lastEmoji ?: error("missing lastEmoji.")
addCode(emoji, line) addCode(emoji, line)
} }
"sn" -> { "sn" -> {
val emoji = lastEmoji ?: error("missing lastEmoji.") val emoji = lastEmoji ?: error("missing lastEmoji.")
addName(emoji, line) addName(emoji, line)
emoji.unifiedName = line emoji.unifiedName = line
} }
"s" -> { "s" -> {
val emoji = lastEmoji ?: error("missing lastEmoji.") val emoji = lastEmoji ?: error("missing lastEmoji.")
addName(emoji, line) addName(emoji, line)
} }
"t" -> { "t" -> {
val cols = line.split(",", limit = 3) val cols = line.split(",", limit = 3)
if (cols.size != 3) error("invalid tone spec. line=$lno $line") if (cols.size != 3) error("invalid tone spec. line=$lno $line")
@ -99,6 +103,7 @@ class EmojiMapLoader(
lastCategory = categoryNameMap[line] lastCategory = categoryNameMap[line]
?: error("missing category name.") ?: error("missing category name.")
} }
"c" -> { "c" -> {
val category = lastCategory val category = lastCategory
?: error("missing lastCategory.") ?: error("missing lastCategory.")
@ -110,6 +115,7 @@ class EmojiMapLoader(
category.emojiList.add(emoji) category.emojiList.add(emoji)
} }
} }
else -> error("unknown header $head") else -> error("unknown header $head")
} }
} catch (ex: Throwable) { } catch (ex: Throwable) {

View File

@ -76,7 +76,7 @@ enum class AdditionalButtonsPosition(
; ;
companion object { companion object {
fun fromIndex(i: Int) = values().find { it.idx == i } ?: Top fun fromIndex(i: Int) = entries.find { it.idx == i } ?: Top
} }
} }
@ -821,7 +821,7 @@ class StatusButtonsViewHolder(
} }
private fun AnkoFlexboxLayout.additionalButtons() { private fun AnkoFlexboxLayout.additionalButtons() {
btnCustomShares = CustomShareTarget.values().map { target -> btnCustomShares = CustomShareTarget.entries.map { target ->
imageButton { imageButton {
background = ContextCompat.getDrawable( background = ContextCompat.getDrawable(
context, context,

View File

@ -256,10 +256,9 @@ class NotificationChannelsInitializer : Initializer<Boolean> {
override fun create(context: Context): Boolean { override fun create(context: Context): Boolean {
context.run { context.run {
val list = NotificationChannels.values()
log.i("createNotificationChannel(s) size=${list.size}")
val notificationManager = NotificationManagerCompat.from(this) val notificationManager = NotificationManagerCompat.from(this)
for (nc in list) { log.i("createNotificationChannel(s) size=${NotificationChannels.entries.size}")
for (nc in NotificationChannels.entries) {
val channel = NotificationChannel( val channel = NotificationChannel(
nc.id, nc.id,
getString(nc.titleId), getString(nc.titleId),

View File

@ -10,9 +10,4 @@ enum class PollingState(val desc: String) {
CheckServerInformation("check server information"), CheckServerInformation("check server information"),
CheckPushSubscription("check push subscription"), CheckPushSubscription("check push subscription"),
CheckNotifications("check notifications"), CheckNotifications("check notifications"),
;
companion object {
val valuesCache = values()
}
} }

View File

@ -4,7 +4,17 @@ import android.app.ActivityManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.work.* import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ForegroundInfo
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequest
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import androidx.work.await
import jp.juggler.subwaytooter.ActMain import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.App1 import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
@ -114,7 +124,7 @@ class PollingWorker2(
private fun stateMapToString(map: Map<PollingState, List<String>>) = private fun stateMapToString(map: Map<PollingState, List<String>>) =
StringBuilder().apply { StringBuilder().apply {
for (state in PollingState.valuesCache) { for (state in PollingState.entries) {
val list = map[state] ?: continue val list = map[state] ?: continue
if (isNotEmpty()) append(" |") if (isNotEmpty()) append(" |")
append(state.desc) append(state.desc)

View File

@ -9,7 +9,6 @@ enum class TrackingType(
NotReply("notReply", PullNotification.TRACKING_NAME_DEFAULT); NotReply("notReply", PullNotification.TRACKING_NAME_DEFAULT);
companion object { companion object {
private val valuesCache = values() fun parseStr(str: String?) = entries.firstOrNull { it.str == str } ?: All
fun parseStr(str: String?) = valuesCache.firstOrNull { it.str == str } ?: All
} }
} }

View File

@ -7,99 +7,102 @@ import jp.juggler.subwaytooter.api.entity.TootNotification
import jp.juggler.subwaytooter.table.PushMessage import jp.juggler.subwaytooter.table.PushMessage
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
private val log = LogCategory("NotificationIconAndColor") private val log = LogCategory("PushMessageIconColor")
enum class PushMessageIconColor( enum class PushMessageIconColor(
@ColorRes val colorRes: Int, @ColorRes val colorRes: Int,
@DrawableRes val iconId: Int, @DrawableRes val iconId: Int,
val keys: Array<String>, val keys: Set<String>,
) { ) {
Favourite( Favourite(
0, 0,
R.drawable.ic_star_outline, R.drawable.ic_star_outline,
arrayOf("favourite"), setOf("favourite"),
), ),
Mention( Mention(
0, 0,
R.drawable.outline_alternate_email_24, R.drawable.outline_alternate_email_24,
arrayOf("mention"), setOf("mention"),
), ),
Reply( Reply(
0, 0,
R.drawable.ic_reply, R.drawable.ic_reply,
arrayOf("reply") setOf("reply")
), ),
Reblog( Reblog(
0, 0,
R.drawable.ic_repeat, R.drawable.ic_repeat,
arrayOf("reblog", "renote"), setOf("reblog", "renote"),
), ),
Quote( Quote(
0, 0,
R.drawable.ic_quote, R.drawable.ic_quote,
arrayOf("quote"), setOf("quote"),
), ),
Follow( Follow(
0, 0,
R.drawable.ic_person_add, R.drawable.ic_person_add,
arrayOf("follow", "followRequestAccepted") setOf("follow", "followRequestAccepted")
), ),
Unfollow( Unfollow(
0, 0,
R.drawable.ic_follow_cross, R.drawable.ic_follow_cross,
arrayOf("unfollow") setOf("unfollow")
), ),
Reaction( Reaction(
0, 0,
R.drawable.outline_add_reaction_24, R.drawable.outline_add_reaction_24,
arrayOf("reaction", "emoji_reaction", "pleroma:emoji_reaction") setOf("reaction", "emoji_reaction", "pleroma:emoji_reaction")
), ),
FollowRequest( FollowRequest(
R.color.colorNotificationAccentFollowRequest, R.color.colorNotificationAccentFollowRequest,
R.drawable.ic_follow_wait, R.drawable.ic_follow_wait,
arrayOf("follow_request", "receiveFollowRequest"), setOf("follow_request", "receiveFollowRequest"),
), ),
Poll( Poll(
0, 0,
R.drawable.outline_poll_24, R.drawable.outline_poll_24,
arrayOf("pollVote", "poll_vote", "poll"), setOf("pollVote", "poll_vote", "poll"),
), ),
Status( Status(
0, 0,
R.drawable.ic_edit, R.drawable.ic_edit,
arrayOf("status", "update", "status_reference") setOf("status", "update", "status_reference")
), ),
AdminSignUp( AdminSignUp(
0, 0,
R.drawable.outline_group_add_24, R.drawable.outline_group_add_24,
arrayOf(TootNotification.TYPE_ADMIN_SIGNUP), setOf(TootNotification.TYPE_ADMIN_SIGNUP),
), ),
AdminReport( AdminReport(
R.color.colorNotificationAccentAdminReport, R.color.colorNotificationAccentAdminReport,
R.drawable.ic_error, R.drawable.ic_error,
arrayOf(TootNotification.TYPE_ADMIN_REPORT), setOf(TootNotification.TYPE_ADMIN_REPORT),
), ),
Unknown( Unknown(
R.color.colorNotificationAccentUnknown, R.color.colorNotificationAccentUnknown,
R.drawable.ic_question, R.drawable.ic_question,
arrayOf("unknown"), setOf("unknown"),
) )
; ;
companion object { companion object {
val map = buildMap { val map = PushMessageIconColor.entries.map { it.keys }.flatten().toSet()
values().forEach { .associateWith { key ->
for (k in it.keys) { val colors = PushMessageIconColor.entries
val old: PushMessageIconColor? = get(k) .filter { it.keys.contains(key) }
if (old != null) { when {
error("NotificationIconAndColor: $k is duplicate: ${it.name} and ${old.name}") colors.isEmpty() -> error("missing color fot key=$key")
} else { colors.size > 1 -> error(
put(k, it) "NotificationIconAndColor: duplicate key $key to ${
} colors.joinToString(", ") { it.name }
}"
)
else -> colors.first()
} }
} }
}
} }
} }

View File

@ -208,7 +208,7 @@ object CustomShare {
fun getCache(target: CustomShareTarget) = cache[target] fun getCache(target: CustomShareTarget) = cache[target]
fun reloadCache(context: Context) { fun reloadCache(context: Context) {
CustomShareTarget.values().forEach { target -> for (target in CustomShareTarget.entries) {
val cn = target.customShareComponentName val cn = target.customShareComponentName
val pair = getInfo(context, cn) val pair = getInfo(context, cn)
cache[target] = pair cache[target] = pair

View File

@ -3,8 +3,20 @@ package jp.juggler.subwaytooter.util
import android.os.SystemClock import android.os.SystemClock
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.* import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.TootApiResultException
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.EntityId
import jp.juggler.subwaytooter.api.entity.InstanceCapability
import jp.juggler.subwaytooter.api.entity.InstanceType
import jp.juggler.subwaytooter.api.entity.TootAccount
import jp.juggler.subwaytooter.api.entity.TootInstance
import jp.juggler.subwaytooter.api.entity.TootPollsType
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.api.entity.TootTag
import jp.juggler.subwaytooter.api.entity.TootVisibility
import jp.juggler.subwaytooter.api.errorApiResult
import jp.juggler.subwaytooter.api.runApiTask2
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
import jp.juggler.subwaytooter.emoji.CustomEmoji import jp.juggler.subwaytooter.emoji.CustomEmoji
import jp.juggler.subwaytooter.getVisibilityString import jp.juggler.subwaytooter.getVisibilityString
@ -14,19 +26,31 @@ import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.table.daoAcctColor import jp.juggler.subwaytooter.table.daoAcctColor
import jp.juggler.subwaytooter.table.daoSavedAccount import jp.juggler.subwaytooter.table.daoSavedAccount
import jp.juggler.subwaytooter.table.daoTagHistory import jp.juggler.subwaytooter.table.daoTagHistory
import jp.juggler.util.*
import jp.juggler.util.coroutine.AppDispatchers import jp.juggler.util.coroutine.AppDispatchers
import jp.juggler.util.data.* import jp.juggler.util.data.JsonArray
import jp.juggler.util.log.* import jp.juggler.util.data.JsonException
import jp.juggler.util.data.JsonObject
import jp.juggler.util.data.buildJsonArray
import jp.juggler.util.data.buildJsonObject
import jp.juggler.util.data.digestSHA256Hex
import jp.juggler.util.data.groupEx
import jp.juggler.util.data.jsonObjectOf
import jp.juggler.util.data.notEmpty
import jp.juggler.util.data.toJsonArray
import jp.juggler.util.log.LogCategory
import jp.juggler.util.log.errorString
import jp.juggler.util.log.showToast
import jp.juggler.util.network.MEDIA_TYPE_JSON import jp.juggler.util.network.MEDIA_TYPE_JSON
import jp.juggler.util.network.toPostRequestBuilder import jp.juggler.util.network.toPostRequestBuilder
import jp.juggler.util.ui.*
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import java.util.* import java.util.Calendar
import java.util.GregorianCalendar
import java.util.Locale
import java.util.TimeZone
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
sealed class PostResult { sealed class PostResult {
@ -110,36 +134,22 @@ class PostImpl(
} }
} }
private var resultStatus: TootStatus? = null // may null, not error
private var resultCredentialTmp: TootAccount? = null
private var resultScheduledStatusSucceeded = false
private suspend fun getCredential(
client: TootApiClient,
parser: TootParser,
): TootApiResult? {
return client.request("/api/v1/accounts/verify_credentials")?.also { result ->
resultCredentialTmp = parser.account(result.jsonObject)
}
}
private suspend fun getWebVisibility( private suspend fun getWebVisibility(
client: TootApiClient, client: TootApiClient,
parser: TootParser, parser: TootParser,
instance: TootInstance, instance: TootInstance,
): TootVisibility? { ): TootVisibility? = when {
if (account.isMisskey || instance.versionGE(TootInstance.VERSION_1_6)) return null account.isMisskey -> null
instance.versionGE(TootInstance.VERSION_1_6) -> null
val r2 = getCredential(client, parser) else -> {
val privacy = parser.account(
val credentialTmp = resultCredentialTmp client.requestOrThrow("/api/v1/accounts/verify_credentials")
?: errorApiResult(r2) .jsonObject
)?.source?.privacy
val privacy = credentialTmp.source?.privacy ?: error(R.string.cant_get_web_setting_visibility)
?: errorApiResult(activity.getString(R.string.cant_get_web_setting_visibility)) TootVisibility.parseMastodon(privacy)
}
return TootVisibility.parseMastodon(privacy)
// may null, not error
} }
private fun checkServerHasVisibility( private fun checkServerHasVisibility(
@ -150,12 +160,7 @@ class PostImpl(
) { ) {
if (actual != extra || checkFun(instance)) return if (actual != extra || checkFun(instance)) return
val strVisibility = extra.getVisibilityString(account.isMisskey) val strVisibility = extra.getVisibilityString(account.isMisskey)
errorApiResult( activity.errorApiResult(R.string.server_has_no_support_of_visibility, strVisibility)
activity.getString(
R.string.server_has_no_support_of_visibility,
strVisibility
)
)
} }
private suspend fun checkVisibility( private suspend fun checkVisibility(
@ -205,7 +210,6 @@ class PostImpl(
"/api/v1/scheduled_statuses/$scheduledId", "/api/v1/scheduled_statuses/$scheduledId",
Request.Builder().delete() Request.Builder().delete()
) )
log.d("delete old scheduled status. result=$result") log.d("delete old scheduled status. result=$result")
delay(2000L) delay(2000L)
} }
@ -271,15 +275,13 @@ class PostImpl(
// Misskeyの場合、NSFWするにはアップロード済みの画像を drive/files/update で更新する // Misskeyの場合、NSFWするにはアップロード済みの画像を drive/files/update で更新する
if (bNSFW) { if (bNSFW) {
val r = client.request( client.requestOrThrow(
"/api/drive/files/update", "/api/drive/files/update",
account.putMisskeyApiToken().apply { account.putMisskeyApiToken().apply {
put("fileId", a.id.toString()) put("fileId", a.id.toString())
put("isSensitive", true) put("isSensitive", true)
} }.toPostRequestBuilder()
.toPostRequestBuilder()
) )
if (r == null || r.error != null) errorApiResult(r)
} }
} }
if (array.isNotEmpty()) json["mediaIds"] = array if (array.isNotEmpty()) json["mediaIds"] = array
@ -361,7 +363,7 @@ class PostImpl(
if (scheduledAt != 0L) { if (scheduledAt != 0L) {
if (!instance.versionGE(TootInstance.VERSION_2_7_0_rc1)) { if (!instance.versionGE(TootInstance.VERSION_2_7_0_rc1)) {
errorApiResult(activity.getString(R.string.scheduled_status_requires_mastodon_2_7_0)) activity.errorApiResult(R.string.scheduled_status_requires_mastodon_2_7_0)
} }
// UTCの日時を渡す // UTCの日時を渡す
val c = GregorianCalendar.getInstance(TimeZone.getTimeZone("UTC")) val c = GregorianCalendar.getInstance(TimeZone.getTimeZone("UTC"))
@ -505,32 +507,28 @@ class PostImpl(
isPosting.set(true) isPosting.set(true)
return try { return try {
withContext(AppDispatchers.MainImmediate) { withContext(AppDispatchers.MainImmediate) {
activity.runApiTask( val (status, scheduled) = activity.runApiTask2(
account, accessInfo = account,
progressSetup = { it.setCanceledOnTouchOutside(false) }, progressSetup = { it.setCanceledOnTouchOutside(false) },
) { client -> ) { client ->
val (instance, ri) = TootInstance.get(client) val instance = TootInstance.getOrThrow(client)
instance ?: return@runApiTask ri
if (instance.instanceType == InstanceType.Pixelfed) { if (instance.instanceType == InstanceType.Pixelfed) {
// Pixelfedは返信に画像を添付できない // Pixelfedは返信に画像を添付できない
if (inReplyToId != null && attachmentList != null) { if (inReplyToId != null && attachmentList != null) {
return@runApiTask TootApiResult(getString(R.string.pixelfed_does_not_allow_reply_with_media)) error(R.string.pixelfed_does_not_allow_reply_with_media)
} }
// Pixelfedの返信ではない投稿は画像添付が必須 // Pixelfedの返信ではない投稿は画像添付が必須
if (inReplyToId == null && attachmentList == null) { if (inReplyToId == null && attachmentList == null) {
return@runApiTask TootApiResult(getString(R.string.pixelfed_does_not_allow_post_without_media)) error(R.string.pixelfed_does_not_allow_post_without_media)
} }
} }
val parser = TootParser(this, account) val parser = TootParser(this, account)
this@PostImpl.visibilityChecked = try { // may null
checkVisibility(client, parser, instance) // may null this@PostImpl.visibilityChecked = checkVisibility(client, parser, instance)
} catch (ex: TootApiResultException) {
return@runApiTask ex.result
}
if (redraftStatusId != null) { if (redraftStatusId != null) {
// 元の投稿を削除する // 元の投稿を削除する
@ -546,10 +544,8 @@ class PostImpl(
} else { } else {
encodeParamsMastodon(json, instance) encodeParamsMastodon(json, instance)
} }
} catch (ex: TootApiResultException) {
return@runApiTask ex.result
} catch (ex: JsonException) { } catch (ex: JsonException) {
log.e(ex, "status encoding failed.") throw IllegalStateException("encoding status failed.", ex)
} }
val bodyString = json.toString() val bodyString = json.toString()
@ -567,67 +563,66 @@ class PostImpl(
} }
} }
when { try {
account.isMisskey -> client.request( val result = when {
"/api/notes/create", account.isMisskey -> client.requestOrThrow(
createRequestBuilder() "/api/notes/create",
) createRequestBuilder()
)
editStatusId != null -> client.request( editStatusId != null -> client.requestOrThrow(
"/api/v1/statuses/$editStatusId", "/api/v1/statuses/$editStatusId",
createRequestBuilder(isPut = true) createRequestBuilder(isPut = true)
) )
else -> client.request( else -> client.requestOrThrow(
"/api/v1/statuses", "/api/v1/statuses",
createRequestBuilder() createRequestBuilder()
) )
}?.also { result -> }
val jsonObject = result.jsonObject val jsonObject = result.jsonObject
when {
if (scheduledAt != 0L && jsonObject != null) { // 予約投稿完了
// {"id":"3","scheduled_at":"2019-01-06T07:08:00.000Z","media_attachments":[]} scheduledAt != 0L && jsonObject != null -> {
resultScheduledStatusSucceeded = true // {"id":"3","scheduled_at":"2019-01-06T07:08:00.000Z","media_attachments":[]}
return@runApiTask result Pair(null, true)
} else { }
val status = parser.status(
when {
account.isMisskey -> jsonObject?.jsonObject("createdNote")
?: jsonObject
else -> jsonObject // 通常投稿完了
} else -> {
) val status = parser.status(
resultStatus = status when {
saveStatusTag(status) account.isMisskey ->
jsonObject?.jsonObject("createdNote")
?: jsonObject
else -> jsonObject
}
)?.also { saveStatusTag(it) }
Pair(status, false)
}
}
} catch (ex: TootApiResultException) {
val errorMessage = ex.result?.error
when {
errorMessage.isNullOrBlank() -> error("(missing error detail)")
errorMessage.contains("HTTP 404") ->
error("$ex\n${activity.getString(R.string.post_404_desc)}")
else -> throw ex
} }
} }
}.let { result -> }
if (result == null) throw CancellationException() when {
scheduled -> PostResult.Scheduled(account)
val status = resultStatus status == null ->
when { error("can't parse status in API result.")
resultScheduledStatusSucceeded ->
PostResult.Scheduled(account)
// 連投してIdempotency が同じだった場合もエラーにはならず、ここを通る // 連投してIdempotency が同じだった場合もエラーにはならず、ここを通る
status != null -> else -> PostResult.Normal(account, status)
PostResult.Normal(account, status)
else -> {
val e = result.error
error(
when {
e.isNullOrBlank() -> "(missing error detail)"
e.contains("HTTP 404") ->
"$e\n${activity.getString(R.string.post_404_desc)}"
else -> e
}
)
}
}
} }
} }
} finally { } finally {

View File

@ -7,7 +7,12 @@ import jp.juggler.util.log.showError
import jp.juggler.util.log.showToast import jp.juggler.util.log.showToast
import jp.juggler.util.ui.ProgressDialogEx import jp.juggler.util.ui.ProgressDialogEx
import jp.juggler.util.ui.dismissSafe import jp.juggler.util.ui.dismissSafe
import kotlinx.coroutines.* import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext import kotlin.coroutines.EmptyCoroutineContext
@ -68,6 +73,7 @@ fun AppCompatActivity.launchAndShowError(
is CancellationException -> { is CancellationException -> {
log.w(errorCaption ?: "launchAndShowError cancelled.") log.w(errorCaption ?: "launchAndShowError cancelled.")
} }
else -> { else -> {
log.e(ex, errorCaption ?: "launchAndShowError failed.") log.e(ex, errorCaption ?: "launchAndShowError failed.")
showError(ex, errorCaption) showError(ex, errorCaption)

View File

@ -9,7 +9,8 @@ import android.util.Base64
import androidx.core.net.toUri import androidx.core.net.toUri
import jp.juggler.util.log.LogCategory import jp.juggler.util.log.LogCategory
import java.security.MessageDigest import java.security.MessageDigest
import java.util.* import java.util.LinkedList
import java.util.Locale
import java.util.regex.Matcher import java.util.regex.Matcher
import java.util.regex.Pattern import java.util.regex.Pattern

View File

@ -1,6 +1,7 @@
package jp.juggler.util.os package jp.juggler.util.os
import android.content.Context import android.content.Context
import androidx.annotation.StringRes
/** /**
* インストゥルメントテストのContextは * インストゥルメントテストのContextは
@ -8,4 +9,12 @@ import android.content.Context
* この場合は元のcontextを補うのがベストだろう * この場合は元のcontextを補うのがベストだろう
*/ */
val Context.applicationContextSafe: Context val Context.applicationContextSafe: Context
get() = applicationContext ?: this get() = try {
applicationContext ?: this
} catch (ex: Throwable) {
// applicationContextへのアクセスは例外を出すことがある
this
}
fun Context.error(@StringRes resId: Int, vararg args: Any?): Nothing =
error(getString(resId, *args))

View File

@ -392,18 +392,17 @@ fun AppCompatActivity.setNavigationBack(toolbar: Toolbar) =
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
} }
val Float.roundPixels get() = (this + 0.5f).toInt()
fun DisplayMetrics.dpFloat(src: Float) = (density * src)
fun DisplayMetrics.dpFloat(src: Int) = (density * src.toFloat())
fun Resources.dpFloat(src: Float) = displayMetrics.dpFloat(src)
fun Resources.dpFloat(src: Int) = displayMetrics.dpFloat(src)
fun Context.dpFloat(src: Float) = resources.dpFloat(src)
fun Context.dpFloat(src: Int) = resources.dpFloat(src)
val Float.roundPixels get()= (this+0.5f).toInt() fun DisplayMetrics.dp(src: Float) = (density * src).roundPixels
fun DisplayMetrics.dpFloat(src:Float) = (density* src) fun DisplayMetrics.dp(src: Int) = (density * src.toFloat()).roundPixels
fun DisplayMetrics.dpFloat(src:Int) = (density*src.toFloat()) fun Resources.dp(src: Float) = displayMetrics.dp(src)
fun Resources.dpFloat(src:Float) = displayMetrics.dpFloat(src) fun Resources.dp(src: Int) = displayMetrics.dp(src)
fun Resources.dpFloat(src:Int) = displayMetrics.dpFloat(src) fun Context.dp(src: Float) = resources.dp(src)
fun Context.dpFloat(src:Float) = resources.dpFloat(src) fun Context.dp(src: Int) = resources.dp(src)
fun Context.dpFloat(src:Int) = resources.dpFloat(src)
fun DisplayMetrics.dp(src:Float) = (density* src).roundPixels
fun DisplayMetrics.dp(src:Int) = (density*src.toFloat()).roundPixels
fun Resources.dp(src:Float) = displayMetrics.dp(src)
fun Resources.dp(src:Int) = displayMetrics.dp(src)
fun Context.dp(src:Float) = resources.dp(src)
fun Context.dp(src:Int) = resources.dp(src)

View File

@ -17,7 +17,7 @@ object Vers {
const val conscryptVersion = "2.5.2" const val conscryptVersion = "2.5.2"
const val coreKtxVersion = "1.12.0" const val coreKtxVersion = "1.12.0"
const val desugarLibVersion = "2.0.3" const val desugarLibVersion = "2.0.3"
const val detektVersion = "1.23.1" const val detektVersion = "1.23.4"
const val emoji2Version = "1.4.0" const val emoji2Version = "1.4.0"
const val glideVersion = "4.15.1" const val glideVersion = "4.15.1"
const val junitVersion = "4.13.2" const val junitVersion = "4.13.2"

View File

@ -15,7 +15,6 @@ android {
defaultConfig { defaultConfig {
minSdk = Vers.stMinSdkVersion minSdk = Vers.stMinSdkVersion
targetSdk = Vers.stTargetSdkVersion
testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true

View File

@ -671,7 +671,7 @@ style:
active: true active: true
OptionalUnit: OptionalUnit:
active: false active: false
OptionalWhenBraces: BracesOnWhenStatements:
active: false active: false
PreferToOverPairSyntax: PreferToOverPairSyntax:
active: false active: false

View File

@ -13,7 +13,6 @@ android {
defaultConfig { defaultConfig {
minSdk = Vers.stMinSdkVersion minSdk = Vers.stMinSdkVersion
targetSdk = Vers.stTargetSdkVersion
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
} }

View File

@ -13,7 +13,6 @@ android {
defaultConfig { defaultConfig {
minSdk = Vers.stMinSdkVersion minSdk = Vers.stMinSdkVersion
targetSdk = Vers.stTargetSdkVersion
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro") consumerProguardFiles("consumer-rules.pro")
} }

View File

@ -31,7 +31,7 @@ android {
) )
} }
} }
packagingOptions { packaging {
resources { resources {
pickFirsts += listOf("META-INF/atomicfu.kotlin_module") pickFirsts += listOf("META-INF/atomicfu.kotlin_module")
} }