diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 443c947d53..86853b55ec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -47,14 +47,18 @@ import org.session.libsession.messaging.mentions.MentionsManager import org.session.libsession.messaging.messages.control.DataExtractionNotification import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage import org.session.libsession.messaging.messages.signal.OutgoingTextMessage +import org.session.libsession.messaging.messages.visible.OpenGroupInvitation import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.open_groups.OpenGroupAPIV2 import org.session.libsession.messaging.sending_receiving.MessageSender +import org.session.libsession.messaging.sending_receiving.MessageSender.send import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel +import org.session.libsession.utilities.Address.Companion.fromSerialized import org.session.libsession.utilities.MediaTypes import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.ListenableFuture import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity @@ -81,6 +85,8 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel.LinkPreviewState import org.thoughtcrime.securesms.loki.utilities.ActivityDispatcher import org.thoughtcrime.securesms.loki.utilities.push +import org.thoughtcrime.securesms.loki.activities.SelectContactsActivity +import org.thoughtcrime.securesms.loki.activities.SelectContactsActivity.Companion.selectedContactsKey import org.thoughtcrime.securesms.loki.utilities.MentionUtilities import org.thoughtcrime.securesms.loki.utilities.toPx import org.thoughtcrime.securesms.mediasend.Media @@ -161,6 +167,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe const val TAKE_PHOTO = 7 const val PICK_GIF = 10 const val PICK_FROM_LIBRARY = 12 + const val INVITE_CONTACTS = 124 } // endregion @@ -579,8 +586,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe // region Interaction override fun onOptionsItemSelected(item: MenuItem): Boolean { - // TODO: Implement - return super.onOptionsItemSelected(item) + return ConversationMenuHelper.onOptionItemSelected(this, item, thread) } // `position` is the adapter position; not the visual position @@ -846,6 +852,25 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } sendAttachments(slideDeck.asAttachments(), body) } + INVITE_CONTACTS -> { + if (!thread.isOpenGroupRecipient) { return } + val extras = intent?.extras ?: return + if (!intent.hasExtra(SelectContactsActivity.selectedContactsKey)) { return } + val selectedContacts = extras.getStringArray(selectedContactsKey)!! + val openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadID) + for (contact in selectedContacts) { + val recipient = Recipient.from(this, fromSerialized(contact), true) + val message = VisibleMessage() + message.sentTimestamp = System.currentTimeMillis() + val openGroupInvitation = OpenGroupInvitation() + openGroupInvitation.name = openGroup!!.name + openGroupInvitation.url = openGroup!!.joinURL + message.openGroupInvitation = openGroupInvitation + val outgoingTextMessage = OutgoingTextMessage.fromOpenGroupInvitation(openGroupInvitation, recipient, message.sentTimestamp) + DatabaseFactory.getSmsDatabase(this).insertMessageOutbox(-1, outgoingTextMessage, message.sentTimestamp!!) + MessageSender.send(message, recipient.address) + } + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt index 7dc18130d7..bf0be129e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt @@ -1,18 +1,49 @@ package org.thoughtcrime.securesms.conversation.v2.menus +import android.annotation.SuppressLint +import android.content.ClipData +import android.content.ClipboardManager import android.content.Context +import android.content.Intent +import android.graphics.BitmapFactory import android.graphics.PorterDuff import android.graphics.PorterDuffColorFilter +import android.os.AsyncTask import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.widget.ImageView import android.widget.TextView +import android.widget.Toast import androidx.annotation.ColorInt +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat import network.loki.messenger.R +import org.session.libsession.avatars.ContactPhoto +import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate +import org.session.libsession.messaging.sending_receiving.MessageSender +import org.session.libsession.messaging.sending_receiving.leave import org.session.libsession.utilities.ExpirationUtil +import org.session.libsession.utilities.GroupUtil.doubleDecodeGroupID +import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.recipients.Recipient +import org.session.libsignal.utilities.Log +import org.session.libsignal.utilities.guava.Optional +import org.session.libsignal.utilities.toHexString +import org.thoughtcrime.securesms.* +import org.thoughtcrime.securesms.conversation.ConversationActivity +import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity +import org.thoughtcrime.securesms.loki.activities.EditClosedGroupActivity.Companion.groupIDKey +import org.thoughtcrime.securesms.loki.activities.SelectContactsActivity import org.thoughtcrime.securesms.loki.utilities.getColorWithID +import org.thoughtcrime.securesms.util.BitmapUtil +import java.io.IOException +import java.lang.ref.WeakReference object ConversationMenuHelper { @@ -61,6 +92,196 @@ object ConversationMenuHelper { } else { inflater.inflate(R.menu.menu_conversation_unmuted, menu) } - // TODO: Implement search + } + + fun onOptionItemSelected(context: Context, item: MenuItem, thread: Recipient): Boolean { + when (item.itemId) { + R.id.menu_view_all_media -> { showAllMedia(context, thread) } + R.id.menu_search -> { search(context) } + R.id.menu_add_shortcut -> { addShortcut(context, thread) } + R.id.menu_expiring_messages -> { showExpiringMessagesDialog(context, thread) } + R.id.menu_expiring_messages_off -> { showExpiringMessagesDialog(context, thread) } + R.id.menu_unblock -> { unblock(context, thread) } + R.id.menu_block -> { block(context, thread) } + R.id.menu_copy_session_id -> { copySessionID(context, thread) } + R.id.menu_edit_group -> { editClosedGroup(context, thread) } + R.id.menu_leave_group -> { leaveClosedGroup(context, thread) } + R.id.menu_invite_to_open_group -> { inviteContacts(context, thread) } + R.id.menu_unmute_notifications -> { unmute(context, thread) } + R.id.menu_mute_notifications -> { mute(context, thread) } + } + return true + } + + private fun showAllMedia(context: Context, thread: Recipient) { + val intent = Intent(context, MediaOverviewActivity::class.java) + intent.putExtra(MediaOverviewActivity.ADDRESS_EXTRA, thread.address) + val activity = context as AppCompatActivity + activity.startActivity(intent) + } + + private fun search(context: Context) { + Toast.makeText(context, "Not yet implemented", Toast.LENGTH_LONG).show() // TODO: Implement + } + + @SuppressLint("StaticFieldLeak") + private fun addShortcut(context: Context, thread: Recipient) { + object : AsyncTask() { + + override fun doInBackground(vararg params: Void?): IconCompat? { + var icon: IconCompat? = null + val contactPhoto = thread.contactPhoto + if (contactPhoto != null) { + try { + var bitmap = BitmapFactory.decodeStream(contactPhoto.openInputStream(context)) + bitmap = BitmapUtil.createScaledBitmap(bitmap, 300, 300) + icon = IconCompat.createWithAdaptiveBitmap(bitmap) + } catch (e: IOException) { + // Do nothing + } + } + if (icon == null) { + icon = IconCompat.createWithResource(context, if (thread.isGroupRecipient) R.mipmap.ic_group_shortcut else R.mipmap.ic_person_shortcut) + } + return icon + } + + override fun onPostExecute(icon: IconCompat?) { + val name = Optional.fromNullable(thread.name) + .or(Optional.fromNullable(thread.profileName)) + .or(thread.toShortString()) + val shortcutInfo = ShortcutInfoCompat.Builder(context, thread.address.serialize() + '-' + System.currentTimeMillis()) + .setShortLabel(name) + .setIcon(icon) + .setIntent(ShortcutLauncherActivity.createIntent(context, thread.address)) + .build() + if (ShortcutManagerCompat.requestPinShortcut(context, shortcutInfo, null)) { + Toast.makeText(context, context.resources.getString(R.string.ConversationActivity_added_to_home_screen), Toast.LENGTH_LONG).show() + } + } + }.execute() + } + + private fun showExpiringMessagesDialog(context: Context, thread: Recipient) { + if (thread.isClosedGroupRecipient) { + val group = DatabaseFactory.getGroupDatabase(context).getGroup(thread.address.toGroupString()).orNull() + if (group?.isActive == false) { return } + } + ExpirationDialog.show(context, thread.expireMessages, ExpirationDialog.OnClickListener { expirationTime: Int -> + DatabaseFactory.getRecipientDatabase(context).setExpireMessages(thread, expirationTime) + val message = ExpirationTimerUpdate(expirationTime) + message.recipient = thread.address.serialize() + message.sentTimestamp = System.currentTimeMillis() + val expiringMessageManager = ApplicationContext.getInstance(context).expiringMessageManager + expiringMessageManager.setExpirationTimer(message) + MessageSender.send(message, thread.address) + val activity = context as AppCompatActivity + activity.invalidateOptionsMenu() + }) + } + + private fun unblock(context: Context, thread: Recipient) { + if (!thread.isContactRecipient) { return } + val title = R.string.ConversationActivity_unblock_this_contact_question + val message = R.string.ConversationActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact + AlertDialog.Builder(context) + .setTitle(title) + .setMessage(message) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.ConversationActivity_unblock) { _, _ -> + DatabaseFactory.getRecipientDatabase(context) + .setBlocked(thread, false) + }.show() + } + + private fun block(context: Context, thread: Recipient) { + if (!thread.isContactRecipient) { return } + val title = R.string.RecipientPreferenceActivity_block_this_contact_question + val message = R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact + AlertDialog.Builder(context) + .setTitle(title) + .setMessage(message) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.RecipientPreferenceActivity_block) { _, _ -> + DatabaseFactory.getRecipientDatabase(context) + .setBlocked(thread, true) + }.show() + } + + private fun copySessionID(context: Context, thread: Recipient) { + if (!thread.isContactRecipient) { return } + val sessionID = thread.address.toString() + val clip = ClipData.newPlainText("Session ID", sessionID) + val activity = context as AppCompatActivity + val manager = activity.getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager + manager.setPrimaryClip(clip) + Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() + } + + private fun editClosedGroup(context: Context, thread: Recipient) { + if (!thread.isClosedGroupRecipient) { return } + val intent = Intent(context, EditClosedGroupActivity::class.java) + val groupID: String = thread.address.toGroupString() + intent.putExtra(groupIDKey, groupID) + context.startActivity(intent) + } + + private fun leaveClosedGroup(context: Context, thread: Recipient) { + if (!thread.isClosedGroupRecipient) { return } + val builder = AlertDialog.Builder(context) + builder.setTitle(context.resources.getString(R.string.ConversationActivity_leave_group)) + builder.setCancelable(true) + val group = DatabaseFactory.getGroupDatabase(context).getGroup(thread.address.toGroupString()).orNull() + val admins = group.admins + val sessionID = TextSecurePreferences.getLocalNumber(context) + val isCurrentUserAdmin = admins.any { it.toString() == sessionID } + val message = if (isCurrentUserAdmin) { + "Because you are the creator of this group it will be deleted for everyone. This cannot be undone." + } else { + context.resources.getString(R.string.ConversationActivity_are_you_sure_you_want_to_leave_this_group) + } + builder.setMessage(message) + builder.setPositiveButton(R.string.yes) { _, _ -> + var groupPublicKey: String? + var isClosedGroup: Boolean + try { + groupPublicKey = doubleDecodeGroupID(thread.address.toString()).toHexString() + isClosedGroup = DatabaseFactory.getLokiAPIDatabase(context).isClosedGroup(groupPublicKey) + } catch (e: IOException) { + groupPublicKey = null + isClosedGroup = false + } + try { + if (isClosedGroup) { + MessageSender.leave(groupPublicKey!!, true) + // TODO: Disable input? + } else { + Toast.makeText(context, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show() + } + } catch (e: Exception) { + Toast.makeText(context, R.string.ConversationActivity_error_leaving_group, Toast.LENGTH_LONG).show() + } + } + builder.setNegativeButton(R.string.no, null) + builder.show() + } + + private fun inviteContacts(context: Context, thread: Recipient) { + if (!thread.isOpenGroupRecipient) { return } + val intent = Intent(context, SelectContactsActivity::class.java) + val activity = context as AppCompatActivity + activity.startActivityForResult(intent, ConversationActivityV2.INVITE_CONTACTS) + } + + private fun unmute(context: Context, thread: Recipient) { + thread.setMuted(0) + DatabaseFactory.getRecipientDatabase(context).setMuted(thread, 0) + } + + private fun mute(context: Context, thread: Recipient) { + MuteDialog.show(context) { until: Long -> + thread.setMuted(until) + DatabaseFactory.getRecipientDatabase(context).setMuted(thread, until) + } } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt index 97233d6a23..e03a92e134 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/control/ExpirationTimerUpdate.kt @@ -33,7 +33,7 @@ class ExpirationTimerUpdate() : ControlMessage() { } } - internal constructor(duration: Int) : this() { + constructor(duration: Int) : this() { this.syncTarget = null this.duration = duration }