feat: add targeting list

This commit is contained in:
Anna 2021-07-05 02:13:14 -04:00
parent be86e353d1
commit 2da9616b2b
21 changed files with 287 additions and 20 deletions

View File

@ -155,4 +155,4 @@
<option name="WRAP_ON_TYPING" value="0" /> <option name="WRAP_ON_TYPING" value="0" />
</codeStyleSettings> </codeStyleSettings>
</code_scheme> </code_scheme>
</component> </component>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="CompilerConfiguration"> <component name="CompilerConfiguration">
<bytecodeTargetLevel target="1.8" /> <bytecodeTargetLevel target="11" />
</component> </component>
</project> </project>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">

View File

@ -3,6 +3,7 @@
<component name="RunConfigurationProducerService"> <component name="RunConfigurationProducerService">
<option name="ignoredProducers"> <option name="ignoredProducers">
<set> <set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" /> <option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" /> <option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" /> <option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />

View File

@ -58,16 +58,16 @@ dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"]) implementation fileTree(dir: "libs", include: ["*.jar"])
// default kotlin android stuff // default kotlin android stuff
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'com.google.android.material:material:1.3.0' implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.3' implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.3' implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.navigation:navigation-fragment-ktx:2.3.3' implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.3' implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
// preference ui objects // preference ui objects
implementation "androidx.preference:preference-ktx:1.1.1" implementation "androidx.preference:preference-ktx:1.1.1"
// libsodium for android // libsodium for android
@ -100,11 +100,11 @@ dependencies {
implementation 'com.leinardi.android:speed-dial:3.1.1' implementation 'com.leinardi.android:speed-dial:3.1.1'
// tests // tests
testImplementation 'junit:junit:4.13' testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
// desugaring // desugaring
implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.multidex:multidex:2.0.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
// sentry // sentry
implementation 'io.sentry:sentry-android:4.0.0' implementation 'io.sentry:sentry-android:4.0.0'
} }

View File

@ -36,6 +36,8 @@ import io.annaclemens.xivchat.model.message.ClientCatchUp
import io.annaclemens.xivchat.model.message.ClientChannel import io.annaclemens.xivchat.model.message.ClientChannel
import io.annaclemens.xivchat.model.message.ClientMessage import io.annaclemens.xivchat.model.message.ClientMessage
import io.annaclemens.xivchat.model.message.ClientPlayerList import io.annaclemens.xivchat.model.message.ClientPlayerList
import io.annaclemens.xivchat.model.message.ClientPreference
import io.annaclemens.xivchat.model.message.ClientPreferences
import io.annaclemens.xivchat.model.message.ClientShutdown import io.annaclemens.xivchat.model.message.ClientShutdown
import io.annaclemens.xivchat.model.message.InputChannel import io.annaclemens.xivchat.model.message.InputChannel
import io.annaclemens.xivchat.model.message.Ping import io.annaclemens.xivchat.model.message.Ping
@ -49,6 +51,8 @@ import io.annaclemens.xivchat.model.message.SuccessMessage
import io.annaclemens.xivchat.ui.friends.FriendListViewModel import io.annaclemens.xivchat.ui.friends.FriendListViewModel
import io.annaclemens.xivchat.ui.messages.MessagesViewModel import io.annaclemens.xivchat.ui.messages.MessagesViewModel
import io.annaclemens.xivchat.ui.servers.ServersViewModel import io.annaclemens.xivchat.ui.servers.ServersViewModel
import io.annaclemens.xivchat.ui.targeting.TargetingListViewModel
import io.annaclemens.xivchat.ui.targeting.TargetingPlayer
import io.annaclemens.xivchat.util.AlertDialogFragment import io.annaclemens.xivchat.util.AlertDialogFragment
import io.annaclemens.xivchat.util.AppKeyExchange import io.annaclemens.xivchat.util.AppKeyExchange
import io.annaclemens.xivchat.util.CharacterSearchResults import io.annaclemens.xivchat.util.CharacterSearchResults
@ -83,6 +87,8 @@ import kotlinx.coroutines.withTimeout
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.util.*
import kotlin.collections.HashSet
class ConnectionService : Service() { class ConnectionService : Service() {
companion object { companion object {
@ -174,7 +180,7 @@ class ConnectionService : Service() {
.build() .build()
} }
override fun onBind(intent: Intent?): IBinder? { override fun onBind(intent: Intent?): IBinder {
return this.binder return this.binder
} }
@ -317,6 +323,9 @@ class ConnectionService : Service() {
this.service.app.findNavController(R.id.nav_host_fragment).navigate(R.id.nav_messages_tabs) this.service.app.findNavController(R.id.nav_host_fragment).navigate(R.id.nav_messages_tabs)
} }
// send protocol preferences
this.sendPreferences(txStream, tx)
// request catch-up or backlog // request catch-up or backlog
this.requestBacklog(txStream, tx) this.requestBacklog(txStream, tx)
@ -504,6 +513,13 @@ class ConnectionService : Service() {
SecretMessage.writeSecretMessage(this@ConnectionRunner.service.app.sodium, txStream, tx, bytes) SecretMessage.writeSecretMessage(this@ConnectionRunner.service.app.sodium, txStream, tx, bytes)
} }
private suspend fun sendPreferences(txStream: ByteWriteChannel, tx: Key) {
val prefs = ClientPreferences(mapOf(
ClientPreference.TargetingListSupport to true,
))
SecretMessage.writeSecretMessage(this.service.app.sodium, txStream, tx, prefs.encode())
}
private class EventLoop(private val runner: ConnectionRunner) { private class EventLoop(private val runner: ConnectionRunner) {
private var loop = true private var loop = true
@ -652,6 +668,34 @@ class ConnectionService : Service() {
vm.friends.value = list.players.toList() vm.friends.value = list.players.toList()
} }
} }
PlayerListType.Targeting -> {
val vm: TargetingListViewModel by this.runner.service.app.viewModels()
this.runner.service.handler.post {
val now = Date()
val data = list.players
val oldTargeting = vm.targeting.value.orEmpty()
val newTargeting = data
.asSequence()
.filter { new -> oldTargeting.all { current -> current.player.name != new.name && current.player.homeWorld != new.homeWorld } }
.map { TargetingPlayer(it, true, now) }
val finalTargeting = oldTargeting
.asSequence()
.map {
var timestamp = it.timestamp
val current = data.any { dataPlayer -> dataPlayer.name == it.player.name && dataPlayer.homeWorld == it.player.homeWorld }
if (current || it.current) {
timestamp = now
}
TargetingPlayer(it.player, current, timestamp)
}
.plus(newTargeting)
.sortedByDescending { it.current }
.sortedByDescending { it.timestamp }
vm.targeting.value = finalTargeting.toList()
}
}
else -> { else -> {
// TODO: other player list types // TODO: other player list types
} }

View File

@ -204,6 +204,7 @@ class MainActivity : AppCompatActivity(), AlertDialogFragment.DialogHost, EditTe
R.id.nav_servers, R.id.nav_servers,
R.id.nav_messages_tabs, R.id.nav_messages_tabs,
R.id.nav_friend_list, R.id.nav_friend_list,
R.id.nav_targeting_list,
R.id.nav_settings, R.id.nav_settings,
R.id.nav_about, R.id.nav_about,
), ),

View File

@ -104,3 +104,33 @@ class ClientChannel(private val channel: InputChannel) {
return buf return buf
} }
} }
class ClientPreferences(private val preferences: Map<ClientPreference, Any>) {
fun encode(): ByteArray {
val packer = MessagePack.newDefaultBufferPacker()
packer.packArrayHeader(1)
packer.packMapHeader(this.preferences.size)
for (entry in this.preferences) {
packer.packByte(entry.key.code.toByte())
packer.packBoolean(entry.value == true) // TODO: fix
}
packer.close()
val bytes = packer.toByteArray()
val buf = ByteArray(1 + bytes.size)
buf[0] = ClientOperation.Preferences.code
bytes.copyInto(buf, 1)
return buf
}
}
enum class ClientPreference(val code: UByte) {
BacklogNewestMessagesFirst(0u),
TargetingListSupport(1u);
companion object {
private val map = values().associateBy(ClientPreference::code)
fun fromCode(code: UByte) = this.map[code]
}
}

View File

@ -6,7 +6,8 @@ enum class PlayerListType(val code: Byte) {
Party(1), Party(1),
Friend(2), Friend(2),
Linkshell(3), Linkshell(3),
CrossLinkshell(4); CrossLinkshell(4),
Targeting(5);
companion object { companion object {
private val map = values().associateBy(PlayerListType::code) private val map = values().associateBy(PlayerListType::code)

View File

@ -0,0 +1,56 @@
package io.annaclemens.xivchat.ui.targeting
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import io.annaclemens.xivchat.R
import io.annaclemens.xivchat.databinding.TargetingListElementBinding
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.time.Instant
import java.util.*
class TargetingListAdapter(private val context: Context) : RecyclerView.Adapter<TargetingListAdapter.ViewHolder>() {
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val binding = TargetingListElementBinding.bind(itemView)
val name = this.binding.targetingName
val timestamp = this.binding.targetingTime
}
private val inflater = LayoutInflater.from(this.context)
private var playerList: List<TargetingPlayer> = emptyList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val itemView = this.inflater.inflate(R.layout.targeting_list_element, parent, false)
return this.ViewHolder(itemView)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val targeting = this.playerList[position]
// TODO: add send tell context menu?
holder.name.text = targeting.player.name ?: this.context.getString(R.string.friend_list_could_not_retrieve)
holder.timestamp.text = if (targeting.current) {
this.context.getString(R.string.targeting_now)
} else {
if (Instant.now().epochSecond - targeting.timestamp.toInstant().epochSecond > 24 * 60 * 60) {
SimpleDateFormat("dd/MM", Locale.getDefault()).format(targeting.timestamp)
} else {
DateFormat.getTimeInstance(DateFormat.SHORT).format(targeting.timestamp)
}
}
holder.name.isEnabled = targeting.current
holder.timestamp.isEnabled = targeting.current
}
override fun getItemCount() = this.playerList.size
fun setTargetingList(list: List<TargetingPlayer>) {
this.playerList = list
}
}

View File

@ -0,0 +1,46 @@
package io.annaclemens.xivchat.ui.targeting
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.viewModels
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import io.annaclemens.xivchat.databinding.FragmentTargetingBinding
import io.annaclemens.xivchat.util.getApp
class TargetingListFragment : Fragment() {
private lateinit var viewAdapter: TargetingListAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val vm: TargetingListViewModel by this.getApp().viewModels()
val binding = FragmentTargetingBinding.inflate(inflater)
this.viewAdapter = TargetingListAdapter(this.requireContext())
binding.targetingList.apply {
this.setHasFixedSize(true)
this.layoutManager = LinearLayoutManager(this.context)
this.adapter = this@TargetingListFragment.viewAdapter
}
this.updateEmptyText(binding, vm.targeting.value.orEmpty().isEmpty())
vm.targeting.observe(this.viewLifecycleOwner) {
this.updateEmptyText(binding, it.orEmpty().isEmpty())
this.viewAdapter.setTargetingList(it.orEmpty())
this.viewAdapter.notifyDataSetChanged()
}
return binding.root
}
private fun updateEmptyText(binding: FragmentTargetingBinding, visible: Boolean) {
binding.textView.visibility = if (visible) {
View.VISIBLE
} else {
View.GONE
}
}
}

View File

@ -0,0 +1,8 @@
package io.annaclemens.xivchat.ui.targeting
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class TargetingListViewModel : ViewModel() {
val targeting: MutableLiveData<List<TargetingPlayer>?> = MutableLiveData()
}

View File

@ -0,0 +1,10 @@
package io.annaclemens.xivchat.ui.targeting
import io.annaclemens.xivchat.model.message.Player
import java.util.*
data class TargetingPlayer(
val player: Player,
val current: Boolean,
val timestamp: Date,
)

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M12,6c3.79,0 7.17,2.13 8.82,5.5C19.17,14.87 15.79,17 12,17s-7.17,-2.13 -8.82,-5.5C4.83,8.13 8.21,6 12,6m0,-2C7,4 2.73,7.11 1,11.5 2.73,15.89 7,19 12,19s9.27,-3.11 11,-7.5C21.27,7.11 17,4 12,4zM12,9c1.38,0 2.5,1.12 2.5,2.5S13.38,14 12,14s-2.5,-1.12 -2.5,-2.5S10.62,9 12,9m0,-2c-2.48,0 -4.5,2.02 -4.5,4.5S9.52,16 12,16s4.5,-2.02 4.5,-4.5S14.48,7 12,7z" />
</vector>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/targeting_empty" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/targetingList"
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.recyclerview.widget.RecyclerView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp">
<TextView
android:id="@+id/targetingName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
tools:text="Goat Goatington" />
<TextView
android:id="@+id/targetingTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAlignment="textEnd"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
tools:text="04:45" />
</LinearLayout>

View File

@ -20,6 +20,10 @@
android:id="@+id/nav_friend_list" android:id="@+id/nav_friend_list"
android:icon="@drawable/people_black_24dp" android:icon="@drawable/people_black_24dp"
android:title="@string/menu_friend_list" /> android:title="@string/menu_friend_list" />
<item
android:id="@+id/nav_targeting_list"
android:icon="@drawable/visibility_black_24dp"
android:title="@string/menu_targeting" />
</group> </group>
<group android:id="@+id/menuSettings"> <group android:id="@+id/menuSettings">
<item <item

View File

@ -89,6 +89,13 @@
android:name="playerWorld" android:name="playerWorld"
app:argType="integer" /> app:argType="integer" />
</fragment> </fragment>
<fragment
android:id="@+id/nav_targeting_list"
android:name="io.annaclemens.xivchat.ui.targeting.TargetingListFragment"
android:label="@string/menu_targeting"
tools:layout="@layout/fragment_targeting" />
<fragment <fragment
android:id="@+id/nav_scanned_servers" android:id="@+id/nav_scanned_servers"
android:name="io.annaclemens.xivchat.ui.servers.scan.ScannedServersFragment" android:name="io.annaclemens.xivchat.ui.servers.scan.ScannedServersFragment"

View File

@ -208,4 +208,7 @@
<string name="servers_scan_searching">Searching for servers…</string> <string name="servers_scan_searching">Searching for servers…</string>
<string name="servers_scan_instructions">Tap on a server below to add it.</string> <string name="servers_scan_instructions">Tap on a server below to add it.</string>
<string name="friend_list_could_not_retrieve">(Could not retrieve)</string> <string name="friend_list_could_not_retrieve">(Could not retrieve)</string>
<string name="menu_targeting">Targeting</string>
<string name="targeting_now">Now</string>
<string name="targeting_empty">No one has targeted you since connecting.</string>
</resources> </resources>

View File

@ -1,16 +1,16 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = "1.4.30" ext.kotlin_version = "1.5.20"
ext.ktor_version = '1.4.0' ext.ktor_version = '1.4.0'
ext.room_version = '2.2.6' ext.room_version = '2.3.0'
repositories { repositories {
google() google()
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.1.2' classpath 'com.android.tools.build:gradle:4.2.2'
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.3" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"

View File

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip