@ -13,10 +13,9 @@ import android.widget.ImageView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import sushi.hardcore.aira.adapters.ChatAdapter
import sushi.hardcore.aira.adapters.FuckRecyclerView
import sushi.hardcore.aira.background_service.*
import sushi.hardcore.aira.databinding.ActivityChatBinding
import sushi.hardcore.aira.databinding.DialogFingerprintsBinding
@ -35,7 +34,108 @@ class ChatActivity : ServiceBoundActivity() {
private var lastLoadedMessageOffset = 0
private val filePicker = registerForActivityResult ( ActivityResultContracts . GetMultipleContents ( ) ) { uris ->
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 ) {
val contact = airaService . contacts [ sessionId ]
val hasPendingMsgs = airaService . pendingMsgs [ sessionId ] ?. size ?: 0 > 0
runOnUiThread {
if ( contact == null ) {
binding . bottomPanel . visibility = View . VISIBLE
} else {
binding . offlineWarning . visibility = View . GONE
if ( hasPendingMsgs ) {
binding . sendingPendingMsgsIndicator . visibility = View . VISIBLE
chatAdapter . removePendingMessages ( )
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
}
}
}
@ -51,7 +151,7 @@ class ChatActivity : ServiceBoundActivity() {
chatAdapter = ChatAdapter ( this @ChatActivity , :: onClickSaveFile )
binding . recyclerChat . apply {
adapter = chatAdapter
layoutManager = LinearLayoutManager( this @ChatActivity , LinearLayoutManager . VERTICAL , false ) . apply {
layoutManager = FuckRecyclerView( this @ChatActivity ) . apply {
stackFromEnd = true
}
addOnScrollListener ( object : RecyclerView . OnScrollListener ( ) {
@ -75,13 +175,11 @@ class ChatActivity : ServiceBoundActivity() {
}
binding . buttonSend . setOnClickListener {
val msg = binding . editMessage . text . toString ( )
airaService . sendTo ( sessionId , Protocol . newMessage ( msg ) )
binding . editMessage . text . clear ( )
chatAdapter . newMessage ( ChatItem ( true , Protocol . newMessage ( msg ) ) )
if ( airaService . contacts . contains ( sessionId ) ) {
lastLoadedMessageOffset += 1
if ( ! airaService . sendOrAddToPending ( sessionId , Protocol . newMessage ( msg ) ) ) {
chatAdapter . newMessage ( ChatItem ( true , 0 , Protocol . newMessage ( msg ) ) )
scrollToBottom( )
}
binding . recyclerChat . smoothScrollToPosition ( chatAdapter . itemCount )
}
binding . buttonAttach . setOnClickListener {
filePicker . launch ( " */* " )
@ -94,9 +192,8 @@ class ChatActivity : ServiceBoundActivity() {
val session = airaService . sessions [ sessionId ]
val contact = airaService . contacts [ sessionId ]
if ( session == null && contact == null ) { //may happen when resuming activity after session disconnect
onDisconnected ( )
hideBottomPanel ( )
} else {
chatAdapter . clear ( )
val avatar = if ( contact == null ) {
displayIconTrustLevel ( false , false )
sessionName = airaService . savedNames [ sessionId ]
@ -116,93 +213,53 @@ class ChatActivity : ServiceBoundActivity() {
binding . toolbar . avatar . setImageAvatar ( image )
}
}
chatAdapter . clear ( )
lastLoadedMessageOffset = 0
if ( contact != null ) {
loadMsgs ( contact . uuid )
}
airaService . savedMsgs [ sessionId ] ?. let {
for ( chatItem in it . asReversed ( ) ) {
chatAdapter . newLoadedMessage ( chatItem )
}
}
airaService . receiveFileTransfers [ sessionId ] ?. let {
if ( it . shouldAsk ) {
it . ask ( this @ChatActivity , ipName )
}
}
binding . recyclerChat . smoothScrollToPosition ( chatAdapter . itemCount )
if ( airaService . isOnline ( sessionId ) ) {
binding . bottomPanel . visibility = View . VISIBLE
binding . recyclerChat . updatePadding ( bottom = 0 )
} else {
onDisconnected ( )
}
airaService . setSeen ( sessionId , true )
}
airaService . uiCallbacks = object : AIRAService . 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 ( )
}
for ( msg in it . asReversed ( ) ) {
chatAdapter . newLoadedMessage ( ChatItem ( msg . outgoing , msg . timestamp , msg . data ) )
}
}
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 )
var hasPendingMsgs = false
airaService . pendingMsgs [ sessionId ] ?. let {
if ( it . size > 0 ) {
hasPendingMsgs = true
for ( msg in it ) {
if ( msg [ 0 ] == Protocol . MESSAGE || msg [ 0 ] == Protocol . FILE ) {
chatAdapter . newMessage ( ChatItem ( true , 0 , msg ) )
}
}
}
}
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 )
}
}
if ( chatAdapter . itemCount > 0 ) {
scrollToBottom ( )
}
airaService . receiveFileTransfers [ sessionId ] ?. let {
if ( it . shouldAsk ) {
it . ask ( this @ChatActivity , ipName )
}
}
override fun onNewMessage ( sessionId : Int , data : ByteArray ) : Boolean {
return if ( this @ChatActivity . sessionId == sessionId ) {
runOnUiThread {
chatAdapter . newMessage ( ChatItem ( false , data ) )
binding . recyclerChat . smoothScrollToPosition ( chatAdapter . itemCount )
}
if ( airaService . contacts . contains ( sessionId ) ) {
lastLoadedMessageOffset += 1
}
! airaService . isAppInBackground
binding . sendingPendingMsgsIndicator . visibility = if ( session == null ) {
if ( contact == null ) {
hideBottomPanel ( )
} else {
false
binding . offlineWarning . visibility = View . VISIBLE
}
}
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
View . GONE
} else {
binding . offlineWarning . visibility = View . GONE
if ( hasPendingMsgs ) {
View . VISIBLE
} else {
false
View . GONE
}
}
airaService . setSeen ( sessionId , true )
}
airaService . uiCallbacks = uiCallbacks
airaService . isAppInBackground = false
}
override fun onServiceDisconnected ( name : ComponentName ? ) { }
@ -210,7 +267,7 @@ class ChatActivity : ServiceBoundActivity() {
}
}
private fun onDisconnected ( ) {
private fun hideBottomPanel ( ) {
val inputManager = getSystemService ( Context . INPUT _METHOD _SERVICE ) as InputMethodManager
inputManager . hideSoftInputFromWindow ( binding . editMessage . windowToken , 0 )
binding . bottomPanel . visibility = View . GONE
@ -253,6 +310,13 @@ class ChatActivity : ServiceBoundActivity() {
}
}
private fun scrollToBottom ( ) {
val target = chatAdapter . itemCount - 1
if ( target >= 0 ) {
binding . recyclerChat . smoothScrollToPosition ( target )
}
}
override fun onCreateOptionsMenu ( menu : Menu ) : Boolean {
menuInflater . inflate ( R . menu . chat _activity , menu )
val contact = airaService . contacts [ sessionId ]
@ -299,6 +363,10 @@ class ChatActivity : ServiceBoundActivity() {
if ( airaService . removeContact ( sessionId ) ) {
invalidateOptionsMenu ( )
displayIconTrustLevel ( false , false )
if ( ! airaService . isOnline ( sessionId ) ) {
hideBottomPanel ( )
binding . offlineWarning . visibility = View . GONE
}
}
}
. setNegativeButton ( R . string . cancel , null )
@ -340,7 +408,7 @@ class ChatActivity : ServiceBoundActivity() {
true
}
R . id . refresh _profile -> {
airaService . send To( sessionId , Protocol . askProfileInfo ( ) )
airaService . send OrAdd ToPending ( sessionId , Protocol . askProfileInfo ( ) )
true
}
else -> super . onOptionsItemSelected ( item )
@ -354,21 +422,12 @@ class ChatActivity : ServiceBoundActivity() {
}
}
override fun onPause ( ) {
super . onPause ( )
lastLoadedMessageOffset = 0
}
private fun onClickSaveFile ( fileName : String , rawUuid : ByteArray ) {
val buffer = AIRADatabase . loadFile ( rawUuid )
if ( buffer == null ) {
Toast . makeText ( this , R . string . loadFile _failed , Toast . LENGTH _SHORT ) . show ( )
} else {
FileUtils . openFileForDownload ( this , fileName ) ?. apply {
write ( buffer )
close ( )
Toast . makeText ( this @ChatActivity , R . string . file _saved , Toast . LENGTH _SHORT ) . show ( )
}
private fun onClickSaveFile ( fileName : String , fileContent : ByteArray ) {
val file = FileUtils . openFileForDownload ( this , fileName )
file . outputStream ?. apply {
write ( fileContent )
close ( )
Toast . makeText ( this @ChatActivity , getString ( R . string . file _saved , file . fileName ) , Toast . LENGTH _SHORT ) . show ( )
}
}