Pending messages
This commit is contained in:
parent
d618eb87c7
commit
5fc3f7d0cf
|
@ -13,17 +13,15 @@ import android.widget.ImageView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import sushi.hardcore.aira.adapters.ChatAdapter
|
import sushi.hardcore.aira.adapters.ChatAdapter
|
||||||
|
import sushi.hardcore.aira.adapters.FuckRecyclerView
|
||||||
import sushi.hardcore.aira.background_service.*
|
import sushi.hardcore.aira.background_service.*
|
||||||
import sushi.hardcore.aira.databinding.ActivityChatBinding
|
import sushi.hardcore.aira.databinding.ActivityChatBinding
|
||||||
import sushi.hardcore.aira.databinding.DialogFingerprintsBinding
|
import sushi.hardcore.aira.databinding.DialogFingerprintsBinding
|
||||||
import sushi.hardcore.aira.databinding.DialogInfoBinding
|
import sushi.hardcore.aira.databinding.DialogInfoBinding
|
||||||
import sushi.hardcore.aira.utils.FileUtils
|
import sushi.hardcore.aira.utils.FileUtils
|
||||||
import sushi.hardcore.aira.utils.StringUtils
|
import sushi.hardcore.aira.utils.StringUtils
|
||||||
import sushi.hardcore.aira.utils.TimeUtils
|
|
||||||
|
|
||||||
class ChatActivity : ServiceBoundActivity() {
|
class ChatActivity : ServiceBoundActivity() {
|
||||||
private external fun generateFingerprint(publicKey: ByteArray): String
|
private external fun generateFingerprint(publicKey: ByteArray): String
|
||||||
|
@ -36,7 +34,108 @@ class ChatActivity : ServiceBoundActivity() {
|
||||||
private var lastLoadedMessageOffset = 0
|
private var lastLoadedMessageOffset = 0
|
||||||
private val filePicker = registerForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris ->
|
private val filePicker = registerForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris ->
|
||||||
if (isServiceInitialized() && uris.size > 0) {
|
if (isServiceInitialized() && uris.size > 0) {
|
||||||
airaService.sendFilesFromUris(sessionId, uris)
|
airaService.sendFilesFromUris(sessionId, uris) { buffer ->
|
||||||
|
chatAdapter.newMessage(ChatItem(true, 0, buffer))
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val uiCallbacks = object : AIRAService.UiCallbacks {
|
||||||
|
override fun onConnectFailed(ip: String, errorMsg: String?) {}
|
||||||
|
override fun onNewSession(sessionId: Int, ip: String) {
|
||||||
|
if (this@ChatActivity.sessionId == sessionId) {
|
||||||
|
runOnUiThread {
|
||||||
|
val contact = airaService.contacts[sessionId]
|
||||||
|
if (contact == null) {
|
||||||
|
binding.bottomPanel.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
binding.offlineWarning.visibility = View.GONE
|
||||||
|
if (airaService.pendingMsgs[sessionId]!!.size > 0) {
|
||||||
|
binding.sendingPendingMsgsIndicator.visibility = View.VISIBLE
|
||||||
|
//remove pending messages
|
||||||
|
reloadHistory(contact)
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
invalidateOptionsMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun onSessionDisconnect(sessionId: Int) {
|
||||||
|
if (this@ChatActivity.sessionId == sessionId) {
|
||||||
|
runOnUiThread {
|
||||||
|
if (airaService.isContact(sessionId)) {
|
||||||
|
binding.offlineWarning.visibility = View.VISIBLE
|
||||||
|
} else {
|
||||||
|
hideBottomPanel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun onNameTold(sessionId: Int, name: String) {
|
||||||
|
if (this@ChatActivity.sessionId == sessionId) {
|
||||||
|
runOnUiThread {
|
||||||
|
sessionName = name
|
||||||
|
binding.toolbar.title.text = name
|
||||||
|
if (avatar == null) {
|
||||||
|
binding.toolbar.avatar.setTextAvatar(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun onAvatarChanged(sessionId: Int, avatar: ByteArray?) {
|
||||||
|
if (this@ChatActivity.sessionId == sessionId) {
|
||||||
|
runOnUiThread {
|
||||||
|
this@ChatActivity.avatar = avatar
|
||||||
|
if (avatar == null) {
|
||||||
|
binding.toolbar.avatar.setTextAvatar(sessionName)
|
||||||
|
} else {
|
||||||
|
binding.toolbar.avatar.setImageAvatar(avatar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun onSent(sessionId: Int, timestamp: Long, buffer: ByteArray) {
|
||||||
|
if (this@ChatActivity.sessionId == sessionId) {
|
||||||
|
if (airaService.isContact(sessionId)) {
|
||||||
|
lastLoadedMessageOffset += 1
|
||||||
|
}
|
||||||
|
runOnUiThread {
|
||||||
|
chatAdapter.newMessage(ChatItem(true, timestamp, buffer))
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun onPendingMessagesSent(sessionId: Int) {
|
||||||
|
if (this@ChatActivity.sessionId == sessionId) {
|
||||||
|
runOnUiThread {
|
||||||
|
binding.sendingPendingMsgsIndicator.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun onNewMessage(sessionId: Int, timestamp: Long, data: ByteArray): Boolean {
|
||||||
|
return if (this@ChatActivity.sessionId == sessionId) {
|
||||||
|
runOnUiThread {
|
||||||
|
chatAdapter.newMessage(ChatItem(false, timestamp, data))
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
|
if (airaService.isContact(sessionId)) {
|
||||||
|
lastLoadedMessageOffset += 1
|
||||||
|
}
|
||||||
|
!airaService.isAppInBackground
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun onAskLargeFiles(sessionId: Int, filesReceiver: FilesReceiver): Boolean {
|
||||||
|
return if (this@ChatActivity.sessionId == sessionId) {
|
||||||
|
runOnUiThread {
|
||||||
|
filesReceiver.ask(this@ChatActivity, sessionName ?: airaService.sessions[sessionId]!!.ip)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +151,7 @@ class ChatActivity : ServiceBoundActivity() {
|
||||||
chatAdapter = ChatAdapter(this@ChatActivity, ::onClickSaveFile)
|
chatAdapter = ChatAdapter(this@ChatActivity, ::onClickSaveFile)
|
||||||
binding.recyclerChat.apply {
|
binding.recyclerChat.apply {
|
||||||
adapter = chatAdapter
|
adapter = chatAdapter
|
||||||
layoutManager = LinearLayoutManager(this@ChatActivity, LinearLayoutManager.VERTICAL, false).apply {
|
layoutManager = FuckRecyclerView(this@ChatActivity).apply {
|
||||||
stackFromEnd = true
|
stackFromEnd = true
|
||||||
}
|
}
|
||||||
addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
|
@ -76,13 +175,11 @@ class ChatActivity : ServiceBoundActivity() {
|
||||||
}
|
}
|
||||||
binding.buttonSend.setOnClickListener {
|
binding.buttonSend.setOnClickListener {
|
||||||
val msg = binding.editMessage.text.toString()
|
val msg = binding.editMessage.text.toString()
|
||||||
airaService.sendTo(sessionId, Protocol.newMessage(msg))
|
|
||||||
binding.editMessage.text.clear()
|
binding.editMessage.text.clear()
|
||||||
chatAdapter.newMessage(ChatItem(true, TimeUtils.getTimestamp(), Protocol.newMessage(msg)))
|
if (!airaService.sendOrAddToPending(sessionId, Protocol.newMessage(msg))) {
|
||||||
if (airaService.contacts.contains(sessionId)) {
|
chatAdapter.newMessage(ChatItem(true, 0, Protocol.newMessage(msg)))
|
||||||
lastLoadedMessageOffset += 1
|
scrollToBottom()
|
||||||
}
|
}
|
||||||
binding.recyclerChat.smoothScrollToPosition(chatAdapter.itemCount)
|
|
||||||
}
|
}
|
||||||
binding.buttonAttach.setOnClickListener {
|
binding.buttonAttach.setOnClickListener {
|
||||||
filePicker.launch("*/*")
|
filePicker.launch("*/*")
|
||||||
|
@ -95,9 +192,8 @@ class ChatActivity : ServiceBoundActivity() {
|
||||||
val session = airaService.sessions[sessionId]
|
val session = airaService.sessions[sessionId]
|
||||||
val contact = airaService.contacts[sessionId]
|
val contact = airaService.contacts[sessionId]
|
||||||
if (session == null && contact == null) { //may happen when resuming activity after session disconnect
|
if (session == null && contact == null) { //may happen when resuming activity after session disconnect
|
||||||
onDisconnected()
|
hideBottomPanel()
|
||||||
} else {
|
} else {
|
||||||
chatAdapter.clear()
|
|
||||||
val avatar = if (contact == null) {
|
val avatar = if (contact == null) {
|
||||||
displayIconTrustLevel(false, false)
|
displayIconTrustLevel(false, false)
|
||||||
sessionName = airaService.savedNames[sessionId]
|
sessionName = airaService.savedNames[sessionId]
|
||||||
|
@ -117,93 +213,32 @@ class ChatActivity : ServiceBoundActivity() {
|
||||||
binding.toolbar.avatar.setImageAvatar(image)
|
binding.toolbar.avatar.setImageAvatar(image)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (contact != null) {
|
reloadHistory(contact)
|
||||||
loadMsgs(contact.uuid)
|
airaService.pendingMsgs[sessionId]?.let {
|
||||||
}
|
for (msg in it) {
|
||||||
airaService.savedMsgs[sessionId]?.let {
|
if (msg[0] == Protocol.MESSAGE ||msg[0] == Protocol.FILE) {
|
||||||
for (chatItem in it.asReversed()) {
|
chatAdapter.newMessage(ChatItem(true, 0, msg))
|
||||||
chatAdapter.newLoadedMessage(chatItem)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (chatAdapter.itemCount > 0) {
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
airaService.receiveFileTransfers[sessionId]?.let {
|
airaService.receiveFileTransfers[sessionId]?.let {
|
||||||
if (it.shouldAsk) {
|
if (it.shouldAsk) {
|
||||||
it.ask(this@ChatActivity, ipName)
|
it.ask(this@ChatActivity, ipName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.recyclerChat.smoothScrollToPosition(chatAdapter.itemCount)
|
if (session == null) {
|
||||||
if (airaService.isOnline(sessionId)) {
|
if (contact == null) {
|
||||||
binding.bottomPanel.visibility = View.VISIBLE
|
hideBottomPanel()
|
||||||
binding.recyclerChat.updatePadding(bottom = 0)
|
} else {
|
||||||
} else {
|
binding.offlineWarning.visibility = View.VISIBLE
|
||||||
onDisconnected()
|
}
|
||||||
}
|
}
|
||||||
airaService.setSeen(sessionId, true)
|
airaService.setSeen(sessionId, true)
|
||||||
}
|
}
|
||||||
airaService.uiCallbacks = object : AIRAService.UiCallbacks {
|
airaService.uiCallbacks = uiCallbacks
|
||||||
override fun onConnectFailed(ip: String, errorMsg: String?) {}
|
|
||||||
override fun onNewSession(sessionId: Int, ip: String) {
|
|
||||||
if (this@ChatActivity.sessionId == sessionId) {
|
|
||||||
runOnUiThread {
|
|
||||||
binding.bottomPanel.visibility = View.VISIBLE
|
|
||||||
invalidateOptionsMenu()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override fun onSessionDisconnect(sessionId: Int) {
|
|
||||||
if (this@ChatActivity.sessionId == sessionId) {
|
|
||||||
runOnUiThread {
|
|
||||||
onDisconnected()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override fun onNameTold(sessionId: Int, name: String) {
|
|
||||||
if (this@ChatActivity.sessionId == sessionId) {
|
|
||||||
runOnUiThread {
|
|
||||||
sessionName = name
|
|
||||||
binding.toolbar.title.text = name
|
|
||||||
if (avatar == null) {
|
|
||||||
binding.toolbar.avatar.setTextAvatar(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override fun onAvatarChanged(sessionId: Int, avatar: ByteArray?) {
|
|
||||||
if (this@ChatActivity.sessionId == sessionId) {
|
|
||||||
runOnUiThread {
|
|
||||||
this@ChatActivity.avatar = avatar
|
|
||||||
if (avatar == null) {
|
|
||||||
binding.toolbar.avatar.setTextAvatar(sessionName)
|
|
||||||
} else {
|
|
||||||
binding.toolbar.avatar.setImageAvatar(avatar)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override fun onNewMessage(sessionId: Int, timestamp: Long, data: ByteArray): Boolean {
|
|
||||||
return if (this@ChatActivity.sessionId == sessionId) {
|
|
||||||
runOnUiThread {
|
|
||||||
chatAdapter.newMessage(ChatItem(false, timestamp, data))
|
|
||||||
binding.recyclerChat.smoothScrollToPosition(chatAdapter.itemCount)
|
|
||||||
}
|
|
||||||
if (airaService.contacts.contains(sessionId)) {
|
|
||||||
lastLoadedMessageOffset += 1
|
|
||||||
}
|
|
||||||
!airaService.isAppInBackground
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override fun onAskLargeFiles(sessionId: Int, filesReceiver: FilesReceiver): Boolean {
|
|
||||||
return if (this@ChatActivity.sessionId == sessionId) {
|
|
||||||
runOnUiThread {
|
|
||||||
filesReceiver.ask(this@ChatActivity, sessionName ?: airaService.sessions[sessionId]!!.ip)
|
|
||||||
}
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
airaService.isAppInBackground = false
|
airaService.isAppInBackground = false
|
||||||
}
|
}
|
||||||
override fun onServiceDisconnected(name: ComponentName?) {}
|
override fun onServiceDisconnected(name: ComponentName?) {}
|
||||||
|
@ -211,7 +246,7 @@ class ChatActivity : ServiceBoundActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onDisconnected() {
|
private fun hideBottomPanel() {
|
||||||
val inputManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
val inputManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
inputManager.hideSoftInputFromWindow(binding.editMessage.windowToken, 0)
|
inputManager.hideSoftInputFromWindow(binding.editMessage.windowToken, 0)
|
||||||
binding.bottomPanel.visibility = View.GONE
|
binding.bottomPanel.visibility = View.GONE
|
||||||
|
@ -254,6 +289,23 @@ class ChatActivity : ServiceBoundActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun reloadHistory(contact: Contact?) {
|
||||||
|
chatAdapter.clear()
|
||||||
|
lastLoadedMessageOffset = 0
|
||||||
|
if (contact != null) {
|
||||||
|
loadMsgs(contact.uuid)
|
||||||
|
}
|
||||||
|
airaService.savedMsgs[sessionId]?.let {
|
||||||
|
for (msg in it.asReversed()) {
|
||||||
|
chatAdapter.newLoadedMessage(ChatItem(msg.outgoing, msg.timestamp, msg.data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scrollToBottom() {
|
||||||
|
binding.recyclerChat.smoothScrollToPosition(chatAdapter.itemCount-1)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.chat_activity, menu)
|
menuInflater.inflate(R.menu.chat_activity, menu)
|
||||||
val contact = airaService.contacts[sessionId]
|
val contact = airaService.contacts[sessionId]
|
||||||
|
@ -341,7 +393,7 @@ class ChatActivity : ServiceBoundActivity() {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.refresh_profile -> {
|
R.id.refresh_profile -> {
|
||||||
airaService.sendTo(sessionId, Protocol.askProfileInfo())
|
airaService.sendOrAddToPending(sessionId, Protocol.askProfileInfo())
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
@ -355,22 +407,12 @@ class ChatActivity : ServiceBoundActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
private fun onClickSaveFile(fileName: String, fileContent: ByteArray) {
|
||||||
super.onPause()
|
val file = FileUtils.openFileForDownload(this, fileName)
|
||||||
lastLoadedMessageOffset = 0
|
file.outputStream?.apply {
|
||||||
}
|
write(fileContent)
|
||||||
|
close()
|
||||||
private fun onClickSaveFile(fileName: String, rawUuid: ByteArray) {
|
Toast.makeText(this@ChatActivity, getString(R.string.file_saved, file.fileName), Toast.LENGTH_SHORT).show()
|
||||||
val buffer = AIRADatabase.loadFile(rawUuid)
|
|
||||||
if (buffer == null) {
|
|
||||||
Toast.makeText(this, R.string.loadFile_failed, Toast.LENGTH_SHORT).show()
|
|
||||||
} else {
|
|
||||||
val file = FileUtils.openFileForDownload(this, fileName)
|
|
||||||
file.outputStream?.apply {
|
|
||||||
write(buffer)
|
|
||||||
close()
|
|
||||||
Toast.makeText(this@ChatActivity, getString(R.string.file_saved, file.fileName), Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,10 +10,12 @@ class ChatItem(val outgoing: Boolean, val timestamp: Long, val data: ByteArray)
|
||||||
const val OUTGOING_FILE = 2
|
const val OUTGOING_FILE = 2
|
||||||
const val INCOMING_FILE = 3
|
const val INCOMING_FILE = 3
|
||||||
}
|
}
|
||||||
val itemType = if (data[0] == Protocol.MESSAGE) {
|
val itemType: Int by lazy {
|
||||||
if (outgoing) OUTGOING_MESSAGE else INCOMING_MESSAGE
|
if (data[0] == Protocol.MESSAGE) {
|
||||||
} else {
|
if (outgoing) OUTGOING_MESSAGE else INCOMING_MESSAGE
|
||||||
if (outgoing) OUTGOING_FILE else INCOMING_FILE
|
} else {
|
||||||
|
if (outgoing) OUTGOING_FILE else INCOMING_FILE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val calendar: Calendar by lazy {
|
val calendar: Calendar by lazy {
|
||||||
|
|
|
@ -67,19 +67,19 @@ class MainActivity : ServiceBoundActivity() {
|
||||||
onlineSessionAdapter.setName(sessionId, name)
|
onlineSessionAdapter.setName(sessionId, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAvatarChanged(sessionId: Int, avatar: ByteArray?) {
|
override fun onAvatarChanged(sessionId: Int, avatar: ByteArray?) {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
onlineSessionAdapter.setAvatar(sessionId, avatar)
|
onlineSessionAdapter.setAvatar(sessionId, avatar)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
override fun onSent(sessionId: Int, timestamp: Long, buffer: ByteArray) {}
|
||||||
|
override fun onPendingMessagesSent(sessionId: Int) {}
|
||||||
override fun onNewMessage(sessionId: Int, timestamp: Long, data: ByteArray): Boolean {
|
override fun onNewMessage(sessionId: Int, timestamp: Long, data: ByteArray): Boolean {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
onlineSessionAdapter.setSeen(sessionId, false)
|
onlineSessionAdapter.setSeen(sessionId, false)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAskLargeFiles(sessionId: Int, filesReceiver: FilesReceiver): Boolean {
|
override fun onAskLargeFiles(sessionId: Int, filesReceiver: FilesReceiver): Boolean {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
filesReceiver.ask(this@MainActivity, airaService.getNameOf(sessionId))
|
filesReceiver.ask(this@MainActivity, airaService.getNameOf(sessionId))
|
||||||
|
|
|
@ -3,8 +3,6 @@ package sushi.hardcore.aira.adapters
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.GradientDrawable
|
import android.graphics.drawable.GradientDrawable
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -16,8 +14,10 @@ import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.updateMargins
|
import androidx.core.view.updateMargins
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import sushi.hardcore.aira.AIRADatabase
|
||||||
import sushi.hardcore.aira.ChatItem
|
import sushi.hardcore.aira.ChatItem
|
||||||
import sushi.hardcore.aira.R
|
import sushi.hardcore.aira.R
|
||||||
|
import sushi.hardcore.aira.background_service.Protocol
|
||||||
import sushi.hardcore.aira.utils.StringUtils
|
import sushi.hardcore.aira.utils.StringUtils
|
||||||
import sushi.hardcore.aira.utils.TimeUtils
|
import sushi.hardcore.aira.utils.TimeUtils
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
|
@ -41,9 +41,7 @@ class ChatAdapter(
|
||||||
|
|
||||||
fun newMessage(chatItem: ChatItem) {
|
fun newMessage(chatItem: ChatItem) {
|
||||||
chatItems.add(chatItem)
|
chatItems.add(chatItem)
|
||||||
Handler(Looper.getMainLooper()).postDelayed({
|
notifyItemChanged(chatItems.size-2)
|
||||||
notifyItemChanged(chatItems.size-2)
|
|
||||||
}, 100)
|
|
||||||
notifyItemInserted(chatItems.size-1)
|
notifyItemInserted(chatItems.size-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,26 +124,44 @@ class ChatAdapter(
|
||||||
bubble.background = backgroundDrawable
|
bubble.background = backgroundDrawable
|
||||||
}
|
}
|
||||||
protected fun showDateAndTime(chatItem: ChatItem, previousChatItem: ChatItem?) {
|
protected fun showDateAndTime(chatItem: ChatItem, previousChatItem: ChatItem?) {
|
||||||
val showDate = if (previousChatItem == null) {
|
var showTextPendingMsg = false
|
||||||
true
|
val textPendingMsg = itemView.findViewById<TextView>(R.id.text_pending_msg)
|
||||||
} else {
|
|
||||||
!TimeUtils.isInTheSameDay(chatItem, previousChatItem)
|
|
||||||
}
|
|
||||||
val textDate = itemView.findViewById<TextView>(R.id.text_date)
|
val textDate = itemView.findViewById<TextView>(R.id.text_date)
|
||||||
|
val textHour = itemView.findViewById<TextView>(R.id.text_hour)
|
||||||
|
val showDate = if (chatItem.timestamp == 0L) {
|
||||||
|
if (previousChatItem == null || previousChatItem.timestamp != 0L) {
|
||||||
|
showTextPendingMsg = true
|
||||||
|
}
|
||||||
|
textHour.visibility = View.GONE
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
textHour.apply {
|
||||||
|
visibility = View.VISIBLE
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
text = StringUtils.toTwoDigits(chatItem.calendar.get(Calendar.HOUR_OF_DAY))+":"+StringUtils.toTwoDigits(chatItem.calendar.get(Calendar.MINUTE))
|
||||||
|
setTextColor(ContextCompat.getColor(context, if (chatItem.outgoing) {
|
||||||
|
R.color.outgoingTimestamp
|
||||||
|
} else {
|
||||||
|
R.color.incomingTimestamp
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
if (previousChatItem == null) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
!TimeUtils.isInTheSameDay(chatItem, previousChatItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
textDate.visibility = if (showDate) {
|
textDate.visibility = if (showDate) {
|
||||||
textDate.text = DateFormat.getDateInstance().format(chatItem.calendar.time)
|
textDate.text = DateFormat.getDateInstance().format(chatItem.calendar.time)
|
||||||
View.VISIBLE
|
View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
View.GONE
|
View.GONE
|
||||||
}
|
}
|
||||||
val textHour = itemView.findViewById<TextView>(R.id.text_hour)
|
textPendingMsg.visibility = if (showTextPendingMsg) {
|
||||||
@SuppressLint("SetTextI18n")
|
View.VISIBLE
|
||||||
textHour.text = StringUtils.toTwoDigits(chatItem.calendar.get(Calendar.HOUR_OF_DAY))+":"+StringUtils.toTwoDigits(chatItem.calendar.get(Calendar.MINUTE))
|
|
||||||
textHour.setTextColor(ContextCompat.getColor(context, if (chatItem.outgoing) {
|
|
||||||
R.color.outgoingTimestamp
|
|
||||||
} else {
|
} else {
|
||||||
R.color.incomingTimestamp
|
View.GONE
|
||||||
}))
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,19 +200,31 @@ class ChatAdapter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal open class FileViewHolder(context: Context, itemView: View, private val onSavingFile: (filename: String, rawUuid: ByteArray) -> Unit): BubbleViewHolder(context, itemView) {
|
internal open class FileViewHolder(context: Context, itemView: View, private val onSavingFile: (fileName: String, fileContent: ByteArray) -> Unit): BubbleViewHolder(context, itemView) {
|
||||||
protected fun bindFile(chatItem: ChatItem, outgoing: Boolean) {
|
protected fun bindFile(chatItem: ChatItem, outgoing: Boolean) {
|
||||||
setBubbleContent(R.layout.file_bubble_content)
|
setBubbleContent(R.layout.file_bubble_content)
|
||||||
val filename = chatItem.data.sliceArray(17 until chatItem.data.size).decodeToString()
|
val buttonSave = itemView.findViewById<ImageButton>(R.id.button_save)
|
||||||
|
val fileName: String
|
||||||
|
if (chatItem.timestamp == 0L) { //pending
|
||||||
|
val file = Protocol.parseSmallFile(chatItem.data)!!
|
||||||
|
fileName = file.rawFileName.decodeToString()
|
||||||
|
buttonSave.setOnClickListener {
|
||||||
|
onSavingFile(fileName, file.fileContent)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fileName = chatItem.data.sliceArray(17 until chatItem.data.size).decodeToString()
|
||||||
|
buttonSave.setOnClickListener {
|
||||||
|
AIRADatabase.loadFile(chatItem.data.sliceArray(1 until 17))?.let {
|
||||||
|
onSavingFile(fileName, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
itemView.findViewById<TextView>(R.id.text_filename).apply {
|
itemView.findViewById<TextView>(R.id.text_filename).apply {
|
||||||
text = filename
|
text = fileName
|
||||||
if (!outgoing) {
|
if (!outgoing) {
|
||||||
highlightColor = ContextCompat.getColor(context, R.color.incomingHighlight)
|
highlightColor = ContextCompat.getColor(context, R.color.incomingHighlight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
itemView.findViewById<ImageButton>(R.id.button_save).setOnClickListener {
|
|
||||||
onSavingFile(filename, chatItem.data.sliceArray(1 until 17))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
package sushi.hardcore.aira.adapters
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
|
||||||
|
//https://stackoverflow.com/questions/36724898/notifyitemchanged-make-the-recyclerview-scroll-and-jump-to-up
|
||||||
|
|
||||||
|
class FuckRecyclerView(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0,
|
||||||
|
defStyleRes: Int = 0,
|
||||||
|
): LinearLayoutManager(context, attrs, defStyleAttr, defStyleRes) {
|
||||||
|
override fun isAutoMeasureEnabled(): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
|
@ -88,6 +88,7 @@ class AIRAService : Service() {
|
||||||
override fun onServiceLost(serviceInfo: NsdServiceInfo?) {}
|
override fun onServiceLost(serviceInfo: NsdServiceInfo?) {}
|
||||||
}
|
}
|
||||||
val savedMsgs = mutableMapOf<Int, MutableList<ChatItem>>()
|
val savedMsgs = mutableMapOf<Int, MutableList<ChatItem>>()
|
||||||
|
val pendingMsgs = mutableMapOf<Int, MutableList<ByteArray>>()
|
||||||
val savedNames = mutableMapOf<Int, String>()
|
val savedNames = mutableMapOf<Int, String>()
|
||||||
val savedAvatars = mutableMapOf<Int, String>()
|
val savedAvatars = mutableMapOf<Int, String>()
|
||||||
val notSeen = mutableListOf<Int>()
|
val notSeen = mutableListOf<Int>()
|
||||||
|
@ -109,6 +110,8 @@ class AIRAService : Service() {
|
||||||
fun onSessionDisconnect(sessionId: Int)
|
fun onSessionDisconnect(sessionId: Int)
|
||||||
fun onNameTold(sessionId: Int, name: String)
|
fun onNameTold(sessionId: Int, name: String)
|
||||||
fun onAvatarChanged(sessionId: Int, avatar: ByteArray?)
|
fun onAvatarChanged(sessionId: Int, avatar: ByteArray?)
|
||||||
|
fun onSent(sessionId: Int, timestamp: Long, buffer: ByteArray)
|
||||||
|
fun onPendingMessagesSent(sessionId: Int)
|
||||||
fun onNewMessage(sessionId: Int, timestamp: Long, data: ByteArray): Boolean
|
fun onNewMessage(sessionId: Int, timestamp: Long, data: ByteArray): Boolean
|
||||||
fun onAskLargeFiles(sessionId: Int, filesReceiver: FilesReceiver): Boolean
|
fun onAskLargeFiles(sessionId: Int, filesReceiver: FilesReceiver): Boolean
|
||||||
}
|
}
|
||||||
|
@ -121,14 +124,20 @@ class AIRAService : Service() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendTo(sessionId: Int, buffer: ByteArray) {
|
fun sendOrAddToPending(sessionId: Int, buffer: ByteArray): Boolean {
|
||||||
serviceHandler.obtainMessage().apply {
|
return if (isOnline(sessionId)) {
|
||||||
what = MESSAGE_SEND_TO
|
serviceHandler.obtainMessage().apply {
|
||||||
data = Bundle().apply {
|
what = MESSAGE_SEND_TO
|
||||||
putInt("sessionId", sessionId)
|
data = Bundle().apply {
|
||||||
putByteArray("buff", buffer)
|
putInt("sessionId", sessionId)
|
||||||
|
putByteArray("buff", buffer)
|
||||||
|
}
|
||||||
|
serviceHandler.sendMessage(this)
|
||||||
}
|
}
|
||||||
serviceHandler.sendMessage(this)
|
true
|
||||||
|
} else {
|
||||||
|
pendingMsgs[sessionId]?.add(buffer)
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,7 +149,7 @@ class AIRAService : Service() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendFilesFromUris(sessionId: Int, uris: List<Uri>) {
|
fun sendFilesFromUris(sessionId: Int, uris: List<Uri>, onPendingSmallFile: ((ByteArray) -> Unit)? = null) {
|
||||||
val files = mutableListOf<SendFile>()
|
val files = mutableListOf<SendFile>()
|
||||||
var useLargeFileTransfer = false
|
var useLargeFileTransfer = false
|
||||||
for (uri in uris) {
|
for (uri in uris) {
|
||||||
|
@ -155,26 +164,22 @@ class AIRAService : Service() {
|
||||||
sendLargeFilesTo(sessionId, files)
|
sendLargeFilesTo(sessionId, files)
|
||||||
} else {
|
} else {
|
||||||
for (file in files) {
|
for (file in files) {
|
||||||
sendSmallFileTo(sessionId, file)
|
var buffer = file.inputStream.readBytes()
|
||||||
|
file.inputStream.close()
|
||||||
|
buffer = Protocol.newFile(file.fileName, buffer)
|
||||||
|
if (!sendOrAddToPending(sessionId, buffer)) {
|
||||||
|
onPendingSmallFile?.let { it(buffer) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendSmallFileTo(sessionId: Int, sendFile: SendFile) {
|
|
||||||
val buffer = sendFile.inputStream.readBytes()
|
|
||||||
sendFile.inputStream.close()
|
|
||||||
sendTo(sessionId, Protocol.newFile(sendFile.fileName, buffer))
|
|
||||||
AIRADatabase.storeFile(contacts[sessionId]?.uuid, buffer)?.let { rawFileUuid ->
|
|
||||||
saveMsg(sessionId, TimeUtils.getTimestamp(), byteArrayOf(Protocol.FILE) + rawFileUuid + sendFile.fileName.toByteArray())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun sendLargeFilesTo(sessionId: Int, files: MutableList<SendFile>) {
|
private fun sendLargeFilesTo(sessionId: Int, files: MutableList<SendFile>) {
|
||||||
if (sendFileTransfers[sessionId] == null && receiveFileTransfers[sessionId] == null) {
|
if (sendFileTransfers[sessionId] == null && receiveFileTransfers[sessionId] == null) {
|
||||||
val filesSender = FilesSender(files, this, notificationManager)
|
val filesSender = FilesSender(files, this, notificationManager)
|
||||||
initFileTransferNotification(sessionId, filesSender.fileTransferNotification, filesSender.files[0])
|
initFileTransferNotification(sessionId, filesSender.fileTransferNotification, filesSender.files[0])
|
||||||
sendFileTransfers[sessionId] = filesSender
|
sendFileTransfers[sessionId] = filesSender
|
||||||
sendTo(sessionId, Protocol.askLargeFiles(files))
|
sendOrAddToPending(sessionId, Protocol.askLargeFiles(files))
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(this, R.string.file_transfer_already_in_progress, Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, R.string.file_transfer_already_in_progress, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
@ -194,7 +199,7 @@ class AIRAService : Service() {
|
||||||
return contacts[sessionId]?.name ?: savedNames[sessionId] ?: sessions[sessionId]!!.ip
|
return contacts[sessionId]?.name ?: savedNames[sessionId] ?: sessions[sessionId]!!.ip
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isContact(sessionId: Int): Boolean {
|
fun isContact(sessionId: Int): Boolean {
|
||||||
return contacts.contains(sessionId)
|
return contacts.contains(sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -208,6 +213,7 @@ class AIRAService : Service() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
savedNames.remove(sessionId)
|
savedNames.remove(sessionId)
|
||||||
|
pendingMsgs[sessionId] = mutableListOf()
|
||||||
savedAvatars.remove(sessionId)
|
savedAvatars.remove(sessionId)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -245,6 +251,7 @@ class AIRAService : Service() {
|
||||||
contacts.remove(sessionId)?.let {
|
contacts.remove(sessionId)?.let {
|
||||||
return if (AIRADatabase.removeContact(it.uuid)) {
|
return if (AIRADatabase.removeContact(it.uuid)) {
|
||||||
savedMsgs[sessionId] = mutableListOf()
|
savedMsgs[sessionId] = mutableListOf()
|
||||||
|
pendingMsgs.remove(sessionId)
|
||||||
savedNames[sessionId] = it.name
|
savedNames[sessionId] = it.name
|
||||||
it.avatar?.let { avatarUuid ->
|
it.avatar?.let { avatarUuid ->
|
||||||
savedAvatars[sessionId] = avatarUuid
|
savedAvatars[sessionId] = avatarUuid
|
||||||
|
@ -328,7 +335,12 @@ class AIRAService : Service() {
|
||||||
val key = session.register(selector, SelectionKey.OP_READ)
|
val key = session.register(selector, SelectionKey.OP_READ)
|
||||||
sessionIdByKey[key] = sessionId
|
sessionIdByKey[key] = sessionId
|
||||||
uiCallbacks?.onNewSession(sessionId, session.ip)
|
uiCallbacks?.onNewSession(sessionId, session.ip)
|
||||||
if (!isContact(sessionId)) {
|
if (isContact(sessionId)) {
|
||||||
|
for (i in 0 until pendingMsgs[sessionId]!!.size) {
|
||||||
|
sendAndSave(sessionId, pendingMsgs[sessionId]!!.removeAt(0))
|
||||||
|
}
|
||||||
|
uiCallbacks?.onPendingMessagesSent(sessionId)
|
||||||
|
} else {
|
||||||
session.encryptAndSend(Protocol.askProfileInfo(), usePadding)
|
session.encryptAndSend(Protocol.askProfileInfo(), usePadding)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -429,10 +441,20 @@ class AIRAService : Service() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendAndSave(sessionId: Int, msg: ByteArray) {
|
private fun sendAndSave(sessionId: Int, buffer: ByteArray) {
|
||||||
sessions[sessionId]?.encryptAndSend(msg, usePadding)
|
sessions[sessionId]?.encryptAndSend(buffer, usePadding)
|
||||||
if (msg[0] == Protocol.MESSAGE) {
|
val timestamp = TimeUtils.getTimestamp()
|
||||||
saveMsg(sessionId, TimeUtils.getTimestamp(), msg)
|
if (buffer[0] == Protocol.MESSAGE) {
|
||||||
|
uiCallbacks?.onSent(sessionId, timestamp, buffer)
|
||||||
|
saveMsg(sessionId, timestamp, buffer)
|
||||||
|
} else if (buffer[0] == Protocol.FILE) {
|
||||||
|
Protocol.parseSmallFile(buffer)?.let { file ->
|
||||||
|
AIRADatabase.storeFile(contacts[sessionId]?.uuid, file.fileContent)?.let { rawFileUuid ->
|
||||||
|
val msg = byteArrayOf(Protocol.FILE) + rawFileUuid + file.rawFileName
|
||||||
|
uiCallbacks?.onSent(sessionId, timestamp, msg)
|
||||||
|
saveMsg(sessionId, timestamp, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -533,6 +555,7 @@ class AIRAService : Service() {
|
||||||
contacts = HashMap(contactList.size)
|
contacts = HashMap(contactList.size)
|
||||||
for (contact in contactList) {
|
for (contact in contactList) {
|
||||||
contacts[sessionCounter] = contact
|
contacts[sessionCounter] = contact
|
||||||
|
pendingMsgs[sessionCounter] = mutableListOf()
|
||||||
if (!contact.seen) {
|
if (!contact.seen) {
|
||||||
notSeen.add(sessionCounter)
|
notSeen.add(sessionCounter)
|
||||||
}
|
}
|
||||||
|
@ -720,10 +743,10 @@ class AIRAService : Service() {
|
||||||
filesReceiver.fileTransferNotification,
|
filesReceiver.fileTransferNotification,
|
||||||
filesReceiver.files[0],
|
filesReceiver.files[0],
|
||||||
)
|
)
|
||||||
sendTo(sessionId, Protocol.acceptLargeFiles())
|
session.encryptAndSend(Protocol.acceptLargeFiles(), usePadding)
|
||||||
}, { filesReceiver ->
|
}, { filesReceiver ->
|
||||||
receiveFileTransfers.remove(sessionId)
|
receiveFileTransfers.remove(sessionId)
|
||||||
sendTo(sessionId, Protocol.abortFilesTransfer())
|
session.encryptAndSend(Protocol.abortFilesTransfer(), usePadding)
|
||||||
filesReceiver.fileTransferNotification.cancel()
|
filesReceiver.fileTransferNotification.cancel()
|
||||||
},
|
},
|
||||||
this,
|
this,
|
||||||
|
|
|
@ -22,7 +22,7 @@ class NotificationBroadcastReceiver: BroadcastReceiver() {
|
||||||
ACTION_MARK_READ -> airaService.setSeen(sessionId, true)
|
ACTION_MARK_READ -> airaService.setSeen(sessionId, true)
|
||||||
ACTION_CANCEL_FILE_TRANSFER -> airaService.cancelFileTransfer(sessionId)
|
ACTION_CANCEL_FILE_TRANSFER -> airaService.cancelFileTransfer(sessionId)
|
||||||
ACTION_REPLY -> RemoteInput.getResultsFromIntent(intent)?.getString(KEY_TEXT_REPLY)?.let { reply ->
|
ACTION_REPLY -> RemoteInput.getResultsFromIntent(intent)?.getString(KEY_TEXT_REPLY)?.let { reply ->
|
||||||
airaService.sendTo(sessionId, Protocol.newMessage(reply))
|
airaService.sendOrAddToPending(sessionId, Protocol.newMessage(reply))
|
||||||
airaService.setSeen(sessionId, true)
|
airaService.setSeen(sessionId, true)
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:bottom="-2dp" android:left="-2dp" android:right="-2dp">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<stroke android:width="1dp" android:color="@color/offline_warning"/>
|
||||||
|
<solid android:color="@color/transparent"/>
|
||||||
|
<padding android:top="15dp"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:bottom="-2dp" android:left="-2dp" android:right="-2dp">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<stroke android:width="1dp" android:color="@color/pending_msg_indicator"/>
|
||||||
|
<solid android:color="@color/transparent"/>
|
||||||
|
<padding android:top="5dp"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:bottom="-2dp" android:left="-2dp" android:right="-2dp">
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<stroke android:width="1dp" android:color="@color/secondary"/>
|
||||||
|
<solid android:color="@color/transparent"/>
|
||||||
|
<padding android:top="15dp"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
|
@ -13,14 +13,79 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||||
app:layout_constraintBottom_toTopOf="@id/bottom_panel"/>
|
app:layout_constraintBottom_toTopOf="@id/offline_warning"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/sending_pending_msgs_indicator"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/recycler_chat"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/bottom_panel"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:gravity="center"
|
||||||
|
android:paddingVertical="10dp"
|
||||||
|
android:background="@drawable/sending_pending_msg_indictor_background">
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:layout_width="50dp"
|
||||||
|
android:layout_height="50dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/sending_pending_messages"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginStart="20dp"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/offline_warning"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/recycler_chat"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/bottom_panel"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:paddingHorizontal="30dp"
|
||||||
|
android:layout_marginBottom="15dp"
|
||||||
|
android:background="@drawable/offline_warning_background">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="50dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:src="@drawable/ic_warning"
|
||||||
|
app:tint="@color/offline_warning"
|
||||||
|
android:contentDescription="@string/warning_desc"
|
||||||
|
android:layout_gravity="center_vertical"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginStart="20dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:text="@string/offline_warning_title"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/offline_warning_msg"
|
||||||
|
android:textColor="#80ffffff"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/bottom_panel"
|
android:id="@+id/bottom_panel"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
android:visibility="gone"
|
|
||||||
android:paddingVertical="5dp"
|
android:paddingVertical="5dp"
|
||||||
android:background="@color/primary">
|
android:background="@color/primary">
|
||||||
|
|
||||||
|
|
|
@ -9,9 +9,19 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center_horizontal"
|
android:layout_gravity="center_horizontal"
|
||||||
android:visibility="gone"
|
|
||||||
android:paddingVertical="5dp"/>
|
android:paddingVertical="5dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_pending_msg"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="20dp"
|
||||||
|
android:layout_marginVertical="10dp"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:text="@string/pending_messages"
|
||||||
|
android:textColor="@color/pending_msg_indicator"
|
||||||
|
android:background="@drawable/pending_msg_indicator_background"/>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/bubble"
|
android:id="@+id/bubble"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|
|
@ -15,4 +15,6 @@
|
||||||
<color name="outgoingTimestamp">#777777</color>
|
<color name="outgoingTimestamp">#777777</color>
|
||||||
<color name="incomingTimestamp">#d7d7d7</color>
|
<color name="incomingTimestamp">#d7d7d7</color>
|
||||||
<color name="incomingHighlight">@color/outgoingTextLink</color>
|
<color name="incomingHighlight">@color/outgoingTextLink</color>
|
||||||
|
<color name="offline_warning">#ff0000</color>
|
||||||
|
<color name="pending_msg_indicator">#909090</color>
|
||||||
</resources>
|
</resources>
|
|
@ -76,7 +76,6 @@
|
||||||
<string name="file_transfer_already_in_progress">Another file transfer is already in progress</string>
|
<string name="file_transfer_already_in_progress">Another file transfer is already in progress</string>
|
||||||
<string name="settings">Settings</string>
|
<string name="settings">Settings</string>
|
||||||
<string name="log_out">Log out</string>
|
<string name="log_out">Log out</string>
|
||||||
<string name="loadFile_failed">File extraction failed</string>
|
|
||||||
<string name="copied">Copied to clipboard !</string>
|
<string name="copied">Copied to clipboard !</string>
|
||||||
<string name="identity">Identity</string>
|
<string name="identity">Identity</string>
|
||||||
<string name="about">About</string>
|
<string name="about">About</string>
|
||||||
|
@ -109,4 +108,9 @@
|
||||||
<string name="clickable_indicator">Clickable indicator</string>
|
<string name="clickable_indicator">Clickable indicator</string>
|
||||||
<string name="avatar">Avatar</string>
|
<string name="avatar">Avatar</string>
|
||||||
<string name="name">Name</string>
|
<string name="name">Name</string>
|
||||||
|
<string name="offline_warning_title">Your contact seems to be offline.</string>
|
||||||
|
<string name="offline_warning_msg">Sent messages will be stored until a connection is established.</string>
|
||||||
|
<string name="warning_desc">Warning icon</string>
|
||||||
|
<string name="pending_messages">Pending messages:</string>
|
||||||
|
<string name="sending_pending_messages">Sending pending messages…</string>
|
||||||
</resources>
|
</resources>
|
|
@ -1,11 +1,11 @@
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = "1.5.10"
|
ext.kotlin_version = "1.5.21"
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.2.2'
|
classpath 'com.android.tools.build:gradle:7.0.0'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.7.1-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
|
||||||
|
|
Loading…
Reference in New Issue