messenger-android

Android graphical user interfaces for GNUnet Messenger
Log | Files | Refs | README | LICENSE

ChatFragment.kt (11880B)


      1 /*
      2    This file is part of GNUnet.
      3    Copyright (C) 2021--2025 GNUnet e.V.
      4 
      5    GNUnet is free software: you can redistribute it and/or modify it
      6    under the terms of the GNU Affero General Public License as published
      7    by the Free Software Foundation, either version 3 of the License,
      8    or (at your option) any later version.
      9 
     10    GNUnet is distributed in the hope that it will be useful, but
     11    WITHOUT ANY WARRANTY; without even the implied warranty of
     12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     13    Affero General Public License for more details.
     14 
     15    You should have received a copy of the GNU Affero General Public License
     16    along with this program.  If not, see <http://www.gnu.org/licenses/>.
     17 
     18    SPDX-License-Identifier: AGPL3.0-or-later
     19  */
     20 /*
     21  * @author t3sserakt
     22  * @file GNUnetMessenger/app/src/main/java/org/gnunet/gnunetmessenger/ui/chat/ChatFragment.kt
     23  */
     24 
     25 package org.gnunet.gnunetmessenger.ui.chat
     26 
     27 import android.os.Bundle
     28 import android.util.Log
     29 import android.view.LayoutInflater
     30 import android.view.Menu
     31 import android.view.MenuInflater
     32 import android.view.MenuItem
     33 import android.view.View
     34 import android.view.ViewGroup
     35 import android.widget.Button
     36 import android.widget.EditText
     37 import android.widget.Toast
     38 import androidx.appcompat.app.AppCompatActivity
     39 import androidx.core.view.MenuHost
     40 import androidx.core.view.MenuProvider
     41 import androidx.fragment.app.Fragment
     42 import androidx.lifecycle.Observer
     43 import androidx.lifecycle.lifecycleScope
     44 import androidx.navigation.fragment.findNavController
     45 import androidx.navigation.fragment.navArgs
     46 import androidx.recyclerview.widget.LinearLayoutManager
     47 import androidx.recyclerview.widget.RecyclerView
     48 import kotlinx.coroutines.launch
     49 import org.gnunet.gnunetmessenger.MainActivity
     50 import org.gnunet.gnunetmessenger.R
     51 import org.gnunet.gnunetmessenger.model.ChatContact
     52 import org.gnunet.gnunetmessenger.model.ChatContext
     53 import org.gnunet.gnunetmessenger.viewmodel.ChatMenuViewModel
     54 import org.gnunet.gnunetmessenger.model.ChatMessage
     55 import org.gnunet.gnunetmessenger.model.ChatMessageType
     56 import org.gnunet.gnunetmessenger.model.MessageKind
     57 import org.gnunet.gnunetmessenger.service.GnunetChat
     58 import org.gnunet.gnunetmessenger.ui.adapters.ChatMessageAdapter
     59 import org.gnunet.gnunetmessenger.viewmodel.ChatViewModel
     60 
     61 class ChatFragment : Fragment(R.layout.fragment_chat) {
     62 
     63     private lateinit var recyclerView: RecyclerView
     64     private lateinit var adapter: ChatMessageAdapter
     65     private val args: ChatFragmentArgs by navArgs()
     66     private lateinit var chatViewModel: ChatViewModel
     67     private lateinit var chatContext: ChatContext
     68     private lateinit var chatMenuViewModel: ChatMenuViewModel
     69     private lateinit var mainActivity: MainActivity
     70     private lateinit var gnunetChat: GnunetChat
     71 
     72     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
     73         super.onViewCreated(view, savedInstanceState)
     74 
     75         val menuHost = requireActivity() as MenuHost
     76 
     77 
     78         menuHost.addMenuProvider(object : MenuProvider {
     79             override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
     80 
     81 
     82                 menuInflater.inflate(R.menu.chat_menu, menu)
     83 
     84                 val chatGroup = gnunetChat.getGroupFromContext(chatContext)
     85 
     86                 if (null != chatGroup) {
     87 
     88                     gnunetChat.iterateGroupContacts(chatGroup) { _, contact ->
     89                         val itemId = View.generateViewId()
     90                         chatMenuViewModel.contactMenuIds[itemId] = contact
     91                         menu.add(Menu.NONE, itemId, 0, "👤 ${contact.name}")
     92                         0
     93                     }
     94                 }
     95             }
     96 
     97             override fun onPrepareMenu(menu: Menu) {
     98                 val contact = gnunetChat.getContextContact(chatContext)
     99                 val isBlocked = gnunetChat.isContactBlocked(contact)
    100                 val isGroup = gnunetChat.isGroup(chatContext)
    101                 val isPlatform = gnunetChat.isPlatform(chatContext)
    102                 val blockItem = menu.findItem(R.id.menu_block_contact)
    103 
    104                 blockItem.isVisible = !isGroup || !isPlatform
    105                 blockItem.title = if (isBlocked) "Unblock Contact" else "Block Contact"
    106 
    107                 menu.findItem(R.id.menu_invite_members)?.isVisible = isGroup || isPlatform
    108             }
    109 
    110             override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
    111                 val mainActivity = requireActivity() as MainActivity
    112                 val gnunetChat = mainActivity.getGnunetChatInstance()
    113                 val handle = mainActivity.getChatHandle()
    114 
    115                 val contact = chatMenuViewModel.contactMenuIds[menuItem.itemId]
    116                 if (contact != null) {
    117                     val action = ChatFragmentDirections.actionChatFragmentToContactFragment(contact)
    118                     findNavController().navigate(action)
    119                     return true
    120                 }
    121 
    122                 return when (menuItem.itemId) {
    123                     R.id.menu_share_identity -> {
    124                         val action = ChatFragmentDirections.actionChatFragmentToLobbyDisplayFragment(lobbyId = gnunetChat.getProfileKey(handle),
    125                             // lifetime not used here
    126                             lifetime = "0")
    127                         findNavController().navigate(action)
    128                         true
    129                     }
    130                     R.id.menu_block_contact -> {
    131                         val contact = gnunetChat.getContextContact(chatContext)
    132                         val isBlocked = gnunetChat.isContactBlocked(contact)
    133                         gnunetChat.setContactBlocked(contact, !isBlocked)
    134                         contact.blocked = !isBlocked
    135                         requireActivity().invalidateOptionsMenu()
    136                         true
    137                     }
    138                     R.id.menu_leave_chat -> {
    139                         val contact = gnunetChat.getContextContact(chatContext)
    140                         gnunetChat.deleteContact(contact)
    141                         true
    142                     }
    143                     R.id.menu_invite_members -> {
    144                         val chatGroup = gnunetChat.getGroupFromContext(chatContext)
    145                         if (null != chatGroup) {
    146                             val action = ChatFragmentDirections.actionChatFragmentToMemberListFragment(chatGroup)
    147                             findNavController().navigate(action)
    148                         }
    149                         true
    150                     }
    151                     else -> false
    152                 }
    153             }
    154         }, viewLifecycleOwner)
    155         val title = try {
    156             when {
    157                 gnunetChat.isGroup(chatContext) || gnunetChat.isPlatform(chatContext) -> {
    158                     gnunetChat.getGroupFromContext(chatContext)?.name
    159                         ?: getString(R.string.placeholder_label_chat)
    160                 }
    161                 else -> {
    162                     gnunetChat.getContextContact(chatContext).name
    163                 }
    164             }
    165         } catch (t: Throwable) {
    166             // The server died (or the binder hasn't reattached yet). Bounce
    167             // the user back to the chat list instead of letting the
    168             // IllegalStateException kill the whole app on the main thread.
    169             Log.w("ChatFragment", "Server unreachable while opening chat; popping back stack", t)
    170             Toast.makeText(
    171                 requireContext(),
    172                 getString(R.string.toast_server_unreachable),
    173                 Toast.LENGTH_SHORT
    174             ).show()
    175             runCatching { findNavController().popBackStack() }
    176             return
    177         }
    178         (requireActivity() as AppCompatActivity).supportActionBar?.title = title
    179     }
    180 
    181     override fun onCreateView(
    182         inflater: LayoutInflater, container: ViewGroup?,
    183         savedInstanceState: Bundle?
    184     ): View? {
    185         val view = inflater.inflate(R.layout.fragment_chat, container, false)
    186         mainActivity = activity as MainActivity
    187         gnunetChat = mainActivity.getGnunetChatInstance()
    188 
    189         chatContext = args.chatContext
    190         chatViewModel = mainActivity.getChatViewModel(chatContext)!!
    191         chatMenuViewModel = mainActivity.getChatMenuViewModel(chatContext)!!
    192         recyclerView = view.findViewById(R.id.chatRecyclerView)
    193         recyclerView.layoutManager = LinearLayoutManager(requireContext())
    194 
    195         adapter = ChatMessageAdapter()
    196         recyclerView.adapter = adapter
    197 
    198         adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
    199             override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
    200                 recyclerView.scrollToPosition(adapter.itemCount - 1)
    201             }
    202         })
    203 
    204         chatViewModel.messages.observe(viewLifecycleOwner, Observer { messages ->
    205             Log.d(
    206                 "ChatFragment",
    207                 "observer: count=${messages.size} " +
    208                     "types=${messages.map { it.type }} " +
    209                     "texts=${messages.map { it.text.take(8) }}"
    210             )
    211             adapter.submitList(messages)
    212             recyclerView.scrollToPosition(messages.size - 1)
    213         })
    214 
    215         // If the ViewModel has no messages (e.g., after account switch cleared
    216         // state), reload history from libgnunetchat via native iteration.
    217         if (chatViewModel.messages.value.isNullOrEmpty()) {
    218             viewLifecycleOwner.lifecycleScope.launch {
    219                 try {
    220                     val profileKey = gnunetChat.getProfileKey(mainActivity.getChatHandle())
    221                     val history = gnunetChat.iterateContextMessages(chatContext)
    222                     Log.d(
    223                         "ChatFragment",
    224                         "iterate: profileKey='${profileKey.take(16)}...' " +
    225                             "(len=${profileKey.length}) historySize=${history.size}"
    226                     )
    227                     for (msg in history) {
    228                         val sKey = msg.sender?.key ?: ""
    229                         val isOwn = sKey.isNotBlank() && sKey == profileKey
    230                         Log.d(
    231                             "ChatFragment",
    232                             "iterate.msg: senderKey='${sKey.take(16)}...' " +
    233                                 "(len=${sKey.length}) isOwn=$isOwn text='${msg.text}'"
    234                         )
    235                         val typed = msg.copy(type = if (isOwn) ChatMessageType.OWN else ChatMessageType.OTHER)
    236                         chatViewModel.addMessage(typed)
    237                     }
    238                     Log.d("ChatFragment", "Loaded ${history.size} messages from native")
    239                 } catch (t: Throwable) {
    240                     Log.w("ChatFragment", "iterateContextMessages failed", t)
    241                 }
    242             }
    243         }
    244 
    245         view.findViewById<Button>(R.id.sendButton).setOnClickListener {
    246             val input = view.findViewById<EditText>(R.id.inputMessage)
    247             val text = input.text.toString()
    248             if (text.isNotBlank()) {
    249                 val newMessage = ChatMessage(
    250                     chatContext = chatContext,
    251                     text = text,
    252                     timestamp = System.currentTimeMillis(),
    253                     sender = ChatContact(chatContext, mainActivity.currentAccount?.name ?: ""),
    254                     kind = MessageKind.TEXT,
    255                     type = ChatMessageType.OWN
    256                 )
    257                 chatViewModel.addMessage(newMessage)
    258                 Log.d(
    259                     "ChatFragment",
    260                     "send: nativeCtxPtr=${chatContext.nativeContextPointer} " +
    261                         "userPtr=${chatContext.userPointer} isGroup=${chatContext.isGroup} " +
    262                         "textLen=${text.length}"
    263                 )
    264                 gnunetChat.sendText(chatContext, text)
    265                 input.text.clear()
    266             }
    267         }
    268 
    269         return view
    270     }
    271 }