mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-27 12:05:22 +00:00
Menu redesign (#958)
* feat: Menu redesign * Add bottomsheet * Handle default peek height * Smooth out setting peek height * Move contacts prep to util * Dialog layout tweaks * Contact grouping tweaks * Add new message dialog * Add public key input delegate * Add create group dialog * Add join community dialog * Handle dialog back navigation * Enter community url tab tweaks * Scan QR code tab refactor * Scan qr code refactor * Direct and community tabs refactor * Add session id copy context menu item * Set dialog background colours * Set full dialog background colour * Minor tweaks * Add closed group contact search * Cleanup * Add content descriptions * Resize community chips * Fix new conversation screen paddings * Fix fade in/out of join community screen * Prevent creating conversation with empty public key * Resize and position create group loader * Fix back nav after creating direct message conversation * Fix inter-screen transitions * Fix new conversation background colours * Fix background colours * Rename contact list header for clarity * Bug fixes * Enable scrolling of Enter Session ID tab of the new message dialog * Minor refactor * Switch to child fragment manager * Fix member search on create group screen Co-authored-by: charles <charles@oxen.io>
This commit is contained in:
parent
3fcd972c2a
commit
fbd1721eaf
@ -30,6 +30,7 @@ dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'com.google.android.material:material:1.2.1'
|
||||
implementation 'com.google.android:flexbox:2.0.1'
|
||||
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||
|
@ -22,7 +22,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import network.loki.messenger.util.InputBarButtonDrawableMatcher.Companion.inputButtonWithDrawable
|
||||
import network.loki.messenger.util.NewConversationButtonDrawableMatcher.Companion.newConversationButtonWithDrawable
|
||||
import org.hamcrest.Matcher
|
||||
import org.hamcrest.Matchers.allOf
|
||||
import org.hamcrest.Matchers.not
|
||||
@ -39,7 +38,6 @@ import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBar
|
||||
import org.thoughtcrime.securesms.home.HomeActivity
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@LargeTest
|
||||
class HomeActivityTests {
|
||||
@ -90,8 +88,8 @@ class HomeActivityTests {
|
||||
}
|
||||
|
||||
private fun goToMyChat() {
|
||||
onView(newConversationButtonWithDrawable(R.drawable.ic_plus)).perform(ViewActions.click())
|
||||
onView(newConversationButtonWithDrawable(R.drawable.ic_message)).perform(ViewActions.click())
|
||||
onView(withId(R.id.newConversationButton)).perform(ViewActions.click())
|
||||
onView(withId(R.id.createPrivateChatButton)).perform(ViewActions.click())
|
||||
// new chat
|
||||
onView(withId(R.id.publicKeyEditText)).perform(ViewActions.closeSoftKeyboard())
|
||||
onView(withId(R.id.copyButton)).perform(ViewActions.click())
|
||||
|
@ -5,24 +5,6 @@ import androidx.annotation.DrawableRes
|
||||
import org.hamcrest.Description
|
||||
import org.hamcrest.TypeSafeMatcher
|
||||
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarButton
|
||||
import org.thoughtcrime.securesms.home.NewConversationButtonSetView
|
||||
|
||||
class NewConversationButtonDrawableMatcher(@DrawableRes private val expectedId: Int): TypeSafeMatcher<View>() {
|
||||
|
||||
companion object {
|
||||
@JvmStatic fun newConversationButtonWithDrawable(@DrawableRes expectedId: Int) = NewConversationButtonDrawableMatcher(expectedId)
|
||||
}
|
||||
|
||||
override fun describeTo(description: Description?) {
|
||||
description?.appendText("with drawable on button with resource id: $expectedId")
|
||||
}
|
||||
|
||||
override fun matchesSafely(item: View): Boolean {
|
||||
if (item !is NewConversationButtonSetView.Button) return false
|
||||
|
||||
return item.getIconID() == expectedId
|
||||
}
|
||||
}
|
||||
|
||||
class InputBarButtonDrawableMatcher(@DrawableRes private val expectedId: Int): TypeSafeMatcher<View>() {
|
||||
|
||||
|
@ -140,23 +140,10 @@
|
||||
android:name="org.thoughtcrime.securesms.preferences.QRCodeActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.dms.CreatePrivateChatActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.groups.CreateClosedGroupActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.groups.EditClosedGroupActivity"
|
||||
android:label="@string/activity_edit_closed_group_title"
|
||||
android:screenOrientation="portrait" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.groups.JoinPublicChatActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/Theme.Session.DayNight.FlatActionBar" />
|
||||
<activity
|
||||
android:name="org.thoughtcrime.securesms.onboarding.SeedActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewConfiguration
|
||||
import android.widget.FrameLayout
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.sign
|
||||
|
||||
/**
|
||||
* Layout to wrap a scrollable component inside a ViewPager2. Provided as a solution to the problem
|
||||
* where pages of ViewPager2 have nested scrollable elements that scroll in the same direction as
|
||||
* ViewPager2. The scrollable element needs to be the immediate and only child of this host layout.
|
||||
*
|
||||
* This solution has limitations when using multiple levels of nested scrollable elements
|
||||
* (e.g. a horizontal RecyclerView in a vertical RecyclerView in a horizontal ViewPager2).
|
||||
*/
|
||||
class NestedScrollableHost : FrameLayout {
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||
|
||||
private var touchSlop = 0
|
||||
private var initialX = 0f
|
||||
private var initialY = 0f
|
||||
private val parentViewPager: ViewPager2?
|
||||
get() {
|
||||
var v: View? = parent as? View
|
||||
while (v != null && v !is ViewPager2) {
|
||||
v = v.parent as? View
|
||||
}
|
||||
return v as? ViewPager2
|
||||
}
|
||||
|
||||
private val child: View? get() = if (childCount > 0) getChildAt(0) else null
|
||||
|
||||
init {
|
||||
touchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
||||
}
|
||||
|
||||
private fun canChildScroll(orientation: Int, delta: Float): Boolean {
|
||||
val direction = -delta.sign.toInt()
|
||||
return when (orientation) {
|
||||
0 -> child?.canScrollHorizontally(direction) ?: false
|
||||
1 -> child?.canScrollVertically(direction) ?: false
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInterceptTouchEvent(e: MotionEvent): Boolean {
|
||||
handleInterceptTouchEvent(e)
|
||||
return super.onInterceptTouchEvent(e)
|
||||
}
|
||||
|
||||
private fun handleInterceptTouchEvent(e: MotionEvent) {
|
||||
val orientation = parentViewPager?.orientation ?: return
|
||||
|
||||
// Early return if child can't scroll in same direction as parent
|
||||
if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (e.action == MotionEvent.ACTION_DOWN) {
|
||||
initialX = e.x
|
||||
initialY = e.y
|
||||
parent.requestDisallowInterceptTouchEvent(true)
|
||||
} else if (e.action == MotionEvent.ACTION_MOVE) {
|
||||
val dx = e.x - initialX
|
||||
val dy = e.y - initialY
|
||||
val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL
|
||||
|
||||
// assuming ViewPager2 touch-slop is 2x touch-slop of child
|
||||
val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f
|
||||
val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f
|
||||
|
||||
if (scaledDx > touchSlop || scaledDy > touchSlop) {
|
||||
if (isVpHorizontal == (scaledDy > scaledDx)) {
|
||||
// Gesture is perpendicular, allow all parents to intercept
|
||||
parent.requestDisallowInterceptTouchEvent(false)
|
||||
} else {
|
||||
// Gesture is parallel, query child if movement in that direction is possible
|
||||
if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) {
|
||||
// Child can scroll, disallow all parents to intercept
|
||||
parent.requestDisallowInterceptTouchEvent(true)
|
||||
} else {
|
||||
// Child cannot scroll, allow all parents to intercept
|
||||
parent.requestDisallowInterceptTouchEvent(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
package org.thoughtcrime.securesms.conversation.start
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import network.loki.messenger.databinding.ContactSectionHeaderBinding
|
||||
import network.loki.messenger.databinding.ViewContactBinding
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||
|
||||
sealed class ContactListItem {
|
||||
class Header(val name: String) : ContactListItem()
|
||||
class Contact(val recipient: Recipient, val displayName: String) : ContactListItem()
|
||||
}
|
||||
|
||||
class ContactListAdapter(
|
||||
private val context: Context,
|
||||
private val glide: GlideRequests,
|
||||
private val listener: (Recipient) -> Unit
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
var items = listOf<ContactListItem>()
|
||||
set(value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private object ViewType {
|
||||
const val Contact = 0
|
||||
const val Header = 1
|
||||
}
|
||||
|
||||
class ContactViewHolder(private val binding: ViewContactBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(contact: ContactListItem.Contact, glide: GlideRequests, listener: (Recipient) -> Unit) {
|
||||
binding.profilePictureView.root.glide = glide
|
||||
binding.profilePictureView.root.update(contact.recipient)
|
||||
binding.nameTextView.text = contact.displayName
|
||||
binding.root.setOnClickListener { listener(contact.recipient) }
|
||||
}
|
||||
|
||||
fun unbind() {
|
||||
binding.profilePictureView.root.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
class HeaderViewHolder(
|
||||
private val binding: ContactSectionHeaderBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: ContactListItem.Header) {
|
||||
with(binding) {
|
||||
label.text = item.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return items.size
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
||||
super.onViewRecycled(holder)
|
||||
if (holder is ContactViewHolder) {
|
||||
holder.unbind()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when (items[position]) {
|
||||
is ContactListItem.Header -> ViewType.Header
|
||||
else -> ViewType.Contact
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return if (viewType == ViewType.Contact) {
|
||||
ContactViewHolder(
|
||||
ViewContactBinding.inflate(LayoutInflater.from(context), parent, false)
|
||||
)
|
||||
} else {
|
||||
HeaderViewHolder(
|
||||
ContactSectionHeaderBinding.inflate(LayoutInflater.from(context), parent, false)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
|
||||
val item = items[position]
|
||||
if (viewHolder is ContactViewHolder) {
|
||||
viewHolder.bind(item as ContactListItem.Contact, glide, listener)
|
||||
} else if (viewHolder is HeaderViewHolder) {
|
||||
viewHolder.bind(item as ContactListItem.Header)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package org.thoughtcrime.securesms.conversation.start
|
||||
|
||||
interface NewConversationDelegate {
|
||||
fun onNewMessageSelected()
|
||||
fun onCreateGroupSelected()
|
||||
fun onJoinCommunitySelected()
|
||||
fun onContactSelected(address: String)
|
||||
fun onDialogBackPressed()
|
||||
fun onDialogClosePressed()
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
package org.thoughtcrime.securesms.conversation.start
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Intent
|
||||
import android.content.res.Resources
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.commit
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
import org.thoughtcrime.securesms.dms.NewMessageFragment
|
||||
import org.thoughtcrime.securesms.groups.CreateGroupFragment
|
||||
import org.thoughtcrime.securesms.groups.JoinCommunityFragment
|
||||
|
||||
@AndroidEntryPoint
|
||||
class NewConversationFragment : BottomSheetDialogFragment(), NewConversationDelegate {
|
||||
|
||||
private val defaultPeekHeight: Int by lazy { (Resources.getSystem().displayMetrics.heightPixels * 0.94).toInt() }
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_new_conversation, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
replaceFragment(
|
||||
fragment = NewConversationHomeFragment().apply { delegate = this@NewConversationFragment },
|
||||
fragmentKey = NewConversationHomeFragment::class.java.simpleName
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = BottomSheetDialog(requireContext(), theme)
|
||||
dialog.setOnShowListener {
|
||||
val bottomSheetDialog = it as BottomSheetDialog
|
||||
val parentLayout =
|
||||
bottomSheetDialog.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet)
|
||||
parentLayout?.let { it ->
|
||||
val behaviour = BottomSheetBehavior.from(it)
|
||||
val layoutParams = it.layoutParams
|
||||
layoutParams.height = defaultPeekHeight
|
||||
it.layoutParams = layoutParams
|
||||
behaviour.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
}
|
||||
return dialog
|
||||
}
|
||||
|
||||
override fun onNewMessageSelected() {
|
||||
replaceFragment(NewMessageFragment().apply { delegate = this@NewConversationFragment })
|
||||
}
|
||||
|
||||
override fun onCreateGroupSelected() {
|
||||
replaceFragment(CreateGroupFragment().apply { delegate = this@NewConversationFragment })
|
||||
}
|
||||
|
||||
override fun onJoinCommunitySelected() {
|
||||
replaceFragment(JoinCommunityFragment().apply { delegate = this@NewConversationFragment })
|
||||
}
|
||||
|
||||
override fun onContactSelected(address: String) {
|
||||
val intent = Intent(requireContext(), ConversationActivityV2::class.java)
|
||||
intent.putExtra(ConversationActivityV2.ADDRESS, Address.fromSerialized(address))
|
||||
requireContext().startActivity(intent)
|
||||
requireActivity().overridePendingTransition(R.anim.slide_from_bottom, R.anim.fade_scale_out)
|
||||
}
|
||||
|
||||
override fun onDialogBackPressed() {
|
||||
childFragmentManager.popBackStack()
|
||||
}
|
||||
|
||||
override fun onDialogClosePressed() {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private fun replaceFragment(fragment: Fragment, fragmentKey: String? = null) {
|
||||
childFragmentManager.commit {
|
||||
setCustomAnimations(
|
||||
R.anim.slide_from_right,
|
||||
R.anim.fade_scale_out,
|
||||
0,
|
||||
R.anim.slide_to_right
|
||||
)
|
||||
replace(R.id.new_conversation_fragment_container, fragment)
|
||||
addToBackStack(fragmentKey)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package org.thoughtcrime.securesms.conversation.start
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.FragmentNewConversationHomeBinding
|
||||
import org.session.libsession.messaging.contacts.Contact
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.PublicKeyValidation
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class NewConversationHomeFragment : Fragment() {
|
||||
|
||||
private lateinit var binding: FragmentNewConversationHomeBinding
|
||||
private val viewModel: NewConversationHomeViewModel by viewModels()
|
||||
|
||||
@Inject
|
||||
lateinit var textSecurePreferences: TextSecurePreferences
|
||||
|
||||
lateinit var delegate: NewConversationDelegate
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentNewConversationHomeBinding.inflate(inflater)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.closeButton.setOnClickListener { delegate.onDialogClosePressed() }
|
||||
binding.createPrivateChatButton.setOnClickListener { delegate.onNewMessageSelected() }
|
||||
binding.createClosedGroupButton.setOnClickListener { delegate.onCreateGroupSelected() }
|
||||
binding.joinCommunityButton.setOnClickListener { delegate.onJoinCommunitySelected() }
|
||||
val adapter = ContactListAdapter(requireContext(), GlideApp.with(requireContext())) {
|
||||
delegate.onContactSelected(it.address.serialize())
|
||||
}
|
||||
val unknownSectionTitle = getString(R.string.new_conversation_unknown_contacts_section_title)
|
||||
val recipients = viewModel.recipients.value?.filter { !it.isGroupRecipient && it.address.serialize() != textSecurePreferences.getLocalNumber()!! } ?: emptyList()
|
||||
val contactGroups = recipients.map {
|
||||
val sessionId = it.address.serialize()
|
||||
val contact = DatabaseComponent.get(requireContext()).sessionContactDatabase().getContactWithSessionID(sessionId)
|
||||
val displayName = contact?.displayName(Contact.ContactContext.REGULAR) ?: sessionId
|
||||
ContactListItem.Contact(it, displayName)
|
||||
}.sortedBy { it.displayName }
|
||||
.groupBy { if (PublicKeyValidation.isValid(it.displayName)) unknownSectionTitle else it.displayName.first().uppercase() }
|
||||
.toMutableMap()
|
||||
contactGroups.remove(unknownSectionTitle)?.let { contactGroups.put(unknownSectionTitle, it) }
|
||||
adapter.items = contactGroups.flatMap { entry -> listOf(ContactListItem.Header(entry.key)) + entry.value }
|
||||
binding.contactsRecyclerView.adapter = adapter
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package org.thoughtcrime.securesms.conversation.start
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class NewConversationHomeViewModel @Inject constructor(private val threadDb: ThreadDatabase): ViewModel() {
|
||||
|
||||
private val _recipients = MutableLiveData<List<Recipient>>()
|
||||
val recipients: LiveData<List<Recipient>> = _recipients
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
threadDb.approvedConversationList.use { openCursor ->
|
||||
val reader = threadDb.readerFor(openCursor)
|
||||
val threads = mutableListOf<Recipient>()
|
||||
while (true) {
|
||||
threads += reader.next?.recipient ?: break
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
_recipients.value = threads
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,222 +0,0 @@
|
||||
package org.thoughtcrime.securesms.dms
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityCreatePrivateChatBinding
|
||||
import network.loki.messenger.databinding.FragmentEnterPublicKeyBinding
|
||||
import nl.komponents.kovenant.ui.failUi
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.utilities.PublicKeyValidation
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
|
||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
|
||||
|
||||
class CreatePrivateChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||
private lateinit var binding: ActivityCreatePrivateChatBinding
|
||||
private val adapter = CreatePrivateChatActivityAdapter(this)
|
||||
private var isKeyboardShowing = false
|
||||
set(value) {
|
||||
val hasChanged = (field != value)
|
||||
field = value
|
||||
if (hasChanged) {
|
||||
adapter.isKeyboardShowing = value
|
||||
}
|
||||
}
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
binding = ActivityCreatePrivateChatBinding.inflate(layoutInflater)
|
||||
// Set content view
|
||||
setContentView(binding.root)
|
||||
// Set title
|
||||
supportActionBar!!.title = resources.getString(R.string.activity_create_private_chat_title)
|
||||
// Set up view pager
|
||||
binding.viewPager.adapter = adapter
|
||||
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
||||
binding.rootLayout.viewTreeObserver.addOnGlobalLayoutListener {
|
||||
val diff = binding.rootLayout.rootView.height - binding.rootLayout.height
|
||||
val displayMetrics = this@CreatePrivateChatActivity.resources.displayMetrics
|
||||
val estimatedKeyboardHeight =
|
||||
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 200.0f, displayMetrics)
|
||||
this@CreatePrivateChatActivity.isKeyboardShowing = (diff > estimatedKeyboardHeight)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
private fun showLoader() {
|
||||
binding.loader.visibility = View.VISIBLE
|
||||
binding.loader.animate().setDuration(150).alpha(1.0f).start()
|
||||
}
|
||||
|
||||
private fun hideLoader() {
|
||||
binding.loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() {
|
||||
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
super.onAnimationEnd(animation)
|
||||
binding.loader.visibility = View.GONE
|
||||
}
|
||||
})
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
override fun handleQRCodeScanned(hexEncodedPublicKey: String) {
|
||||
createPrivateChatIfPossible(hexEncodedPublicKey)
|
||||
}
|
||||
|
||||
fun createPrivateChatIfPossible(onsNameOrPublicKey: String) {
|
||||
if (PublicKeyValidation.isValid(onsNameOrPublicKey)) {
|
||||
createPrivateChat(onsNameOrPublicKey)
|
||||
} else {
|
||||
// This could be an ONS name
|
||||
showLoader()
|
||||
SnodeAPI.getSessionID(onsNameOrPublicKey).successUi { hexEncodedPublicKey ->
|
||||
hideLoader()
|
||||
this.createPrivateChat(hexEncodedPublicKey)
|
||||
}.failUi { exception ->
|
||||
hideLoader()
|
||||
var message = resources.getString(R.string.fragment_enter_public_key_error_message)
|
||||
exception.localizedMessage?.let {
|
||||
message = it
|
||||
}
|
||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPrivateChat(hexEncodedPublicKey: String) {
|
||||
val recipient = Recipient.from(this, Address.fromSerialized(hexEncodedPublicKey), false)
|
||||
val intent = Intent(this, ConversationActivityV2::class.java)
|
||||
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
|
||||
intent.setDataAndType(getIntent().data, getIntent().type)
|
||||
val existingThread = DatabaseComponent.get(this).threadDatabase().getThreadIdIfExistsFor(recipient)
|
||||
intent.putExtra(ConversationActivityV2.THREAD_ID, existingThread)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
// region Adapter
|
||||
private class CreatePrivateChatActivityAdapter(val activity: CreatePrivateChatActivity) : FragmentPagerAdapter(activity.supportFragmentManager) {
|
||||
val enterPublicKeyFragment = EnterPublicKeyFragment()
|
||||
var isKeyboardShowing = false
|
||||
set(value) { field = value; enterPublicKeyFragment.isKeyboardShowing = isKeyboardShowing }
|
||||
|
||||
override fun getCount(): Int {
|
||||
return 2
|
||||
}
|
||||
|
||||
override fun getItem(index: Int): Fragment {
|
||||
return when (index) {
|
||||
0 -> enterPublicKeyFragment
|
||||
1 -> {
|
||||
val result = ScanQRCodeWrapperFragment()
|
||||
result.delegate = activity
|
||||
result.message = activity.resources.getString(R.string.activity_create_private_chat_scan_qr_code_explanation)
|
||||
result
|
||||
}
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPageTitle(index: Int): CharSequence? {
|
||||
return when (index) {
|
||||
0 -> activity.resources.getString(R.string.activity_create_private_chat_enter_session_id_tab_title)
|
||||
1 -> activity.resources.getString(R.string.activity_create_private_chat_scan_qr_code_tab_title)
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Enter Public Key Fragment
|
||||
class EnterPublicKeyFragment : Fragment() {
|
||||
private lateinit var binding: FragmentEnterPublicKeyBinding
|
||||
|
||||
var isKeyboardShowing = false
|
||||
set(value) { field = value; handleIsKeyboardShowingChanged() }
|
||||
|
||||
private val hexEncodedPublicKey: String
|
||||
get() {
|
||||
return TextSecurePreferences.getLocalNumber(requireContext())!!
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = FragmentEnterPublicKeyBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
with(binding) {
|
||||
publicKeyEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard
|
||||
publicKeyEditText.setRawInputType(InputType.TYPE_CLASS_TEXT)
|
||||
publicKeyEditText.setOnEditorActionListener { v, actionID, _ ->
|
||||
if (actionID == EditorInfo.IME_ACTION_DONE) {
|
||||
val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(v.windowToken, 0)
|
||||
createPrivateChatIfPossible()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
publicKeyTextView.text = hexEncodedPublicKey
|
||||
copyButton.setOnClickListener { copyPublicKey() }
|
||||
shareButton.setOnClickListener { sharePublicKey() }
|
||||
createPrivateChatButton.setOnClickListener { createPrivateChatIfPossible() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleIsKeyboardShowingChanged() {
|
||||
binding.optionalContentContainer.isVisible = !isKeyboardShowing
|
||||
}
|
||||
|
||||
private fun copyPublicKey() {
|
||||
val clipboard = requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Session ID", hexEncodedPublicKey)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun sharePublicKey() {
|
||||
val intent = Intent()
|
||||
intent.action = Intent.ACTION_SEND
|
||||
intent.putExtra(Intent.EXTRA_TEXT, hexEncodedPublicKey)
|
||||
intent.type = "text/plain"
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun createPrivateChatIfPossible() {
|
||||
val hexEncodedPublicKey = binding.publicKeyEditText.text?.trim().toString()
|
||||
val activity = requireActivity() as CreatePrivateChatActivity
|
||||
activity.createPrivateChatIfPossible(hexEncodedPublicKey)
|
||||
}
|
||||
}
|
||||
// endregion
|
@ -0,0 +1,101 @@
|
||||
package org.thoughtcrime.securesms.dms
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.text.InputType
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.Fragment
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.FragmentEnterPublicKeyBinding
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.QRCodeUtilities
|
||||
import org.thoughtcrime.securesms.util.hideKeyboard
|
||||
import org.thoughtcrime.securesms.util.toPx
|
||||
|
||||
class EnterPublicKeyFragment : Fragment() {
|
||||
private lateinit var binding: FragmentEnterPublicKeyBinding
|
||||
|
||||
var delegate: EnterPublicKeyDelegate? = null
|
||||
|
||||
private val hexEncodedPublicKey: String
|
||||
get() {
|
||||
return TextSecurePreferences.getLocalNumber(requireContext())!!
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = FragmentEnterPublicKeyBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
with(binding) {
|
||||
publicKeyEditText.imeOptions = EditorInfo.IME_ACTION_DONE or 16777216 // Always use incognito keyboard
|
||||
publicKeyEditText.setRawInputType(InputType.TYPE_CLASS_TEXT)
|
||||
publicKeyEditText.setOnEditorActionListener { v, actionID, _ ->
|
||||
if (actionID == EditorInfo.IME_ACTION_DONE) {
|
||||
v.hideKeyboard()
|
||||
handlePublicKeyEntered()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
publicKeyEditText.addTextChangedListener { text -> createPrivateChatButton.isVisible = !text.isNullOrBlank() }
|
||||
publicKeyEditText.setOnFocusChangeListener { _, hasFocus -> optionalContentContainer.isVisible = !hasFocus }
|
||||
mainContainer.setOnTouchListener { _, _ ->
|
||||
binding.optionalContentContainer.isVisible = true
|
||||
publicKeyEditText.clearFocus()
|
||||
publicKeyEditText.hideKeyboard()
|
||||
true
|
||||
}
|
||||
val size = toPx(228, resources)
|
||||
val qrCode = QRCodeUtilities.encode(hexEncodedPublicKey, size, isInverted = false, hasTransparentBackground = false)
|
||||
qrCodeImageView.setImageBitmap(qrCode)
|
||||
publicKeyTextView.text = hexEncodedPublicKey
|
||||
publicKeyTextView.setOnCreateContextMenuListener { contextMenu, view, _ ->
|
||||
contextMenu.add(0, view.id, 0, R.string.copy).setOnMenuItemClickListener {
|
||||
copyPublicKey()
|
||||
true
|
||||
}
|
||||
}
|
||||
copyButton.setOnClickListener { copyPublicKey() }
|
||||
shareButton.setOnClickListener { sharePublicKey() }
|
||||
createPrivateChatButton.setOnClickListener { handlePublicKeyEntered(); publicKeyEditText.hideKeyboard() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyPublicKey() {
|
||||
val clipboard = requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Session ID", hexEncodedPublicKey)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun sharePublicKey() {
|
||||
val intent = Intent()
|
||||
intent.action = Intent.ACTION_SEND
|
||||
intent.putExtra(Intent.EXTRA_TEXT, hexEncodedPublicKey)
|
||||
intent.type = "text/plain"
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun handlePublicKeyEntered() {
|
||||
val hexEncodedPublicKey = binding.publicKeyEditText.text?.trim()?.toString()
|
||||
if (hexEncodedPublicKey.isNullOrEmpty()) return
|
||||
delegate?.handlePublicKeyEntered(hexEncodedPublicKey)
|
||||
}
|
||||
}
|
||||
|
||||
fun interface EnterPublicKeyDelegate {
|
||||
fun handlePublicKeyEntered(publicKey: String)
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
package org.thoughtcrime.securesms.dms
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.FragmentNewMessageBinding
|
||||
import nl.komponents.kovenant.ui.failUi
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.session.libsession.snode.SnodeAPI
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.utilities.PublicKeyValidation
|
||||
import org.thoughtcrime.securesms.conversation.start.NewConversationDelegate
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
|
||||
@AndroidEntryPoint
|
||||
class NewMessageFragment : Fragment() {
|
||||
|
||||
private lateinit var binding: FragmentNewMessageBinding
|
||||
|
||||
lateinit var delegate: NewConversationDelegate
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentNewMessageBinding.inflate(inflater)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.backButton.setOnClickListener { delegate.onDialogBackPressed() }
|
||||
binding.closeButton.setOnClickListener { delegate.onDialogClosePressed() }
|
||||
val onsOrPkDelegate = { onsNameOrPublicKey: String -> createPrivateChatIfPossible(onsNameOrPublicKey)}
|
||||
val adapter = NewMessageFragmentAdapter(
|
||||
parentFragment = this,
|
||||
enterPublicKeyDelegate = onsOrPkDelegate,
|
||||
scanPublicKeyDelegate = onsOrPkDelegate
|
||||
)
|
||||
binding.viewPager.adapter = adapter
|
||||
val mediator = TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, pos ->
|
||||
tab.text = when (pos) {
|
||||
0 -> getString(R.string.activity_create_private_chat_enter_session_id_tab_title)
|
||||
1 -> getString(R.string.activity_create_private_chat_scan_qr_code_tab_title)
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
mediator.attach()
|
||||
}
|
||||
|
||||
private fun createPrivateChatIfPossible(onsNameOrPublicKey: String) {
|
||||
if (PublicKeyValidation.isValid(onsNameOrPublicKey)) {
|
||||
createPrivateChat(onsNameOrPublicKey)
|
||||
} else {
|
||||
// This could be an ONS name
|
||||
showLoader()
|
||||
SnodeAPI.getSessionID(onsNameOrPublicKey).successUi { hexEncodedPublicKey ->
|
||||
hideLoader()
|
||||
createPrivateChat(hexEncodedPublicKey)
|
||||
}.failUi { exception ->
|
||||
hideLoader()
|
||||
var message = getString(R.string.fragment_enter_public_key_error_message)
|
||||
exception.localizedMessage?.let {
|
||||
message = it
|
||||
}
|
||||
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPrivateChat(hexEncodedPublicKey: String) {
|
||||
val recipient = Recipient.from(requireContext(), Address.fromSerialized(hexEncodedPublicKey), false)
|
||||
val intent = Intent(requireContext(), ConversationActivityV2::class.java)
|
||||
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
|
||||
intent.setDataAndType(requireActivity().intent.data, requireActivity().intent.type)
|
||||
val existingThread = DatabaseComponent.get(requireContext()).threadDatabase().getThreadIdIfExistsFor(recipient)
|
||||
intent.putExtra(ConversationActivityV2.THREAD_ID, existingThread)
|
||||
requireContext().startActivity(intent)
|
||||
delegate.onDialogClosePressed()
|
||||
}
|
||||
|
||||
private fun showLoader() {
|
||||
binding.loader.visibility = View.VISIBLE
|
||||
binding.loader.animate().setDuration(150).alpha(1.0f).start()
|
||||
}
|
||||
|
||||
private fun hideLoader() {
|
||||
binding.loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() {
|
||||
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
super.onAnimationEnd(animation)
|
||||
binding.loader.visibility = View.GONE
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package org.thoughtcrime.securesms.dms
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
|
||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
|
||||
|
||||
class NewMessageFragmentAdapter(
|
||||
private val parentFragment: Fragment,
|
||||
private val enterPublicKeyDelegate: EnterPublicKeyDelegate,
|
||||
private val scanPublicKeyDelegate: ScanQRCodeWrapperFragmentDelegate
|
||||
) : FragmentStateAdapter(parentFragment) {
|
||||
|
||||
override fun getItemCount(): Int = 2
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return when (position) {
|
||||
0 -> EnterPublicKeyFragment().apply { delegate = enterPublicKeyDelegate }
|
||||
1 -> ScanQRCodeWrapperFragment().apply { delegate = scanPublicKeyDelegate }
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,145 +0,0 @@
|
||||
package org.thoughtcrime.securesms.groups
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.loader.app.LoaderManager
|
||||
import androidx.loader.content.Loader
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityCreateClosedGroupBinding
|
||||
import nl.komponents.kovenant.ui.failUi
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.messaging.sending_receiving.groupSizeLimit
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.contacts.SelectContactsAdapter
|
||||
import org.thoughtcrime.securesms.contacts.SelectContactsLoader
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.util.fadeIn
|
||||
import org.thoughtcrime.securesms.util.fadeOut
|
||||
|
||||
class CreateClosedGroupActivity : PassphraseRequiredActionBarActivity(), LoaderManager.LoaderCallbacks<List<String>> {
|
||||
private lateinit var binding: ActivityCreateClosedGroupBinding
|
||||
private var isLoading = false
|
||||
set(newValue) { field = newValue; invalidateOptionsMenu() }
|
||||
private var members = listOf<String>()
|
||||
set(value) { field = value; selectContactsAdapter.members = value }
|
||||
private val publicKey: String
|
||||
get() {
|
||||
return TextSecurePreferences.getLocalNumber(this)!!
|
||||
}
|
||||
|
||||
private val selectContactsAdapter by lazy {
|
||||
SelectContactsAdapter(this, GlideApp.with(this))
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val closedGroupCreatedResultCode = 100
|
||||
}
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
binding = ActivityCreateClosedGroupBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
supportActionBar!!.title = resources.getString(R.string.activity_create_closed_group_title)
|
||||
binding.recyclerView.adapter = this.selectContactsAdapter
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
binding.createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() }
|
||||
LoaderManager.getInstance(this).initLoader(0, null, this)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.menu_done, menu)
|
||||
return members.isNotEmpty() && !isLoading
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
override fun onCreateLoader(id: Int, bundle: Bundle?): Loader<List<String>> {
|
||||
return SelectContactsLoader(this, setOf())
|
||||
}
|
||||
|
||||
override fun onLoadFinished(loader: Loader<List<String>>, members: List<String>) {
|
||||
update(members)
|
||||
}
|
||||
|
||||
override fun onLoaderReset(loader: Loader<List<String>>) {
|
||||
update(listOf())
|
||||
}
|
||||
|
||||
private fun update(members: List<String>) {
|
||||
//if there is a Note to self conversation, it loads self in the list, so we need to remove it here
|
||||
this.members = members.minus(publicKey)
|
||||
binding.mainContentContainer.visibility = if (members.isEmpty()) View.GONE else View.VISIBLE
|
||||
binding.emptyStateContainer.visibility = if (members.isEmpty()) View.VISIBLE else View.GONE
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when(item.itemId) {
|
||||
R.id.doneButton -> if (!isLoading) { createClosedGroup() }
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun createNewPrivateChat() {
|
||||
setResult(closedGroupCreatedResultCode)
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun createClosedGroup() {
|
||||
val name = binding.nameEditText.text.trim()
|
||||
if (name.isEmpty()) {
|
||||
return Toast.makeText(this, R.string.activity_create_closed_group_group_name_missing_error, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
if (name.length >= 64) {
|
||||
return Toast.makeText(this, R.string.activity_create_closed_group_group_name_too_long_error, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
val selectedMembers = this.selectContactsAdapter.selectedMembers
|
||||
if (selectedMembers.count() < 1) {
|
||||
return Toast.makeText(this, R.string.activity_create_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
if (selectedMembers.count() >= groupSizeLimit) { // Minus one because we're going to include self later
|
||||
return Toast.makeText(this, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(this)!!
|
||||
isLoading = true
|
||||
binding.loaderContainer.fadeIn()
|
||||
MessageSender.createClosedGroup(name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID ->
|
||||
binding.loaderContainer.fadeOut()
|
||||
isLoading = false
|
||||
val threadID = DatabaseComponent.get(this).threadDatabase().getOrCreateThreadIdFor(Recipient.from(this, Address.fromSerialized(groupID), false))
|
||||
if (!isFinishing) {
|
||||
openConversationActivity(this, threadID, Recipient.from(this, Address.fromSerialized(groupID), false))
|
||||
finish()
|
||||
}
|
||||
}.failUi {
|
||||
binding.loaderContainer.fadeOut()
|
||||
isLoading = false
|
||||
Toast.makeText(this, it.message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
// region Convenience
|
||||
private fun openConversationActivity(context: Context, threadId: Long, recipient: Recipient) {
|
||||
val intent = Intent(context, ConversationActivityV2::class.java)
|
||||
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId)
|
||||
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
// endregion
|
@ -0,0 +1,110 @@
|
||||
package org.thoughtcrime.securesms.groups
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.FragmentCreateGroupBinding
|
||||
import nl.komponents.kovenant.ui.failUi
|
||||
import nl.komponents.kovenant.ui.successUi
|
||||
import org.session.libsession.messaging.sending_receiving.MessageSender
|
||||
import org.session.libsession.messaging.sending_receiving.groupSizeLimit
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.contacts.SelectContactsAdapter
|
||||
import org.thoughtcrime.securesms.conversation.start.NewConversationDelegate
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.util.fadeIn
|
||||
import org.thoughtcrime.securesms.util.fadeOut
|
||||
|
||||
@AndroidEntryPoint
|
||||
class CreateGroupFragment : Fragment() {
|
||||
|
||||
private lateinit var binding: FragmentCreateGroupBinding
|
||||
private val viewModel: CreateGroupViewModel by viewModels()
|
||||
|
||||
lateinit var delegate: NewConversationDelegate
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentCreateGroupBinding.inflate(inflater)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val adapter = SelectContactsAdapter(requireContext(), GlideApp.with(requireContext()))
|
||||
binding.backButton.setOnClickListener { delegate.onDialogBackPressed() }
|
||||
binding.closeButton.setOnClickListener { delegate.onDialogClosePressed() }
|
||||
binding.contactSearch.callbacks = object : KeyboardPageSearchView.Callbacks {
|
||||
override fun onQueryChanged(query: String) {
|
||||
adapter.members = viewModel.filter(query).map { it.address.serialize() }
|
||||
}
|
||||
}
|
||||
binding.createNewPrivateChatButton.setOnClickListener { delegate.onNewMessageSelected() }
|
||||
binding.recyclerView.adapter = adapter
|
||||
var isLoading = false
|
||||
binding.createClosedGroupButton.setOnClickListener {
|
||||
if (isLoading) return@setOnClickListener
|
||||
val name = binding.nameEditText.text.trim()
|
||||
if (name.isEmpty()) {
|
||||
return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_group_name_missing_error, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
if (name.length >= 30) {
|
||||
return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_group_name_too_long_error, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
val selectedMembers = adapter.selectedMembers
|
||||
if (selectedMembers.isEmpty()) {
|
||||
return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_not_enough_group_members_error, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
if (selectedMembers.count() >= groupSizeLimit) { // Minus one because we're going to include self later
|
||||
return@setOnClickListener Toast.makeText(context, R.string.activity_create_closed_group_too_many_group_members_error, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
val userPublicKey = TextSecurePreferences.getLocalNumber(requireContext())!!
|
||||
isLoading = true
|
||||
binding.loaderContainer.fadeIn()
|
||||
MessageSender.createClosedGroup(name.toString(), selectedMembers + setOf( userPublicKey )).successUi { groupID ->
|
||||
binding.loaderContainer.fadeOut()
|
||||
isLoading = false
|
||||
val threadID = DatabaseComponent.get(requireContext()).threadDatabase().getOrCreateThreadIdFor(Recipient.from(requireContext(), Address.fromSerialized(groupID), false))
|
||||
openConversationActivity(
|
||||
requireContext(),
|
||||
threadID,
|
||||
Recipient.from(requireContext(), Address.fromSerialized(groupID), false)
|
||||
)
|
||||
delegate.onDialogClosePressed()
|
||||
}.failUi {
|
||||
binding.loaderContainer.fadeOut()
|
||||
isLoading = false
|
||||
Toast.makeText(context, it.message, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
binding.mainContentGroup.isVisible = !viewModel.recipients.value.isNullOrEmpty()
|
||||
binding.emptyStateGroup.isVisible = viewModel.recipients.value.isNullOrEmpty()
|
||||
viewModel.recipients.observe(viewLifecycleOwner) { recipients ->
|
||||
adapter.members = recipients.map { it.address.serialize() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun openConversationActivity(context: Context, threadId: Long, recipient: Recipient) {
|
||||
val intent = Intent(context, ConversationActivityV2::class.java)
|
||||
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId)
|
||||
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
package org.thoughtcrime.securesms.groups
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class CreateGroupViewModel @Inject constructor(
|
||||
private val threadDb: ThreadDatabase,
|
||||
private val textSecurePreferences: TextSecurePreferences
|
||||
) : ViewModel() {
|
||||
|
||||
private val _recipients = MutableLiveData<List<Recipient>>()
|
||||
val recipients: LiveData<List<Recipient>> = _recipients
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
threadDb.approvedConversationList.use { openCursor ->
|
||||
val reader = threadDb.readerFor(openCursor)
|
||||
val recipients = mutableListOf<Recipient>()
|
||||
while (true) {
|
||||
recipients += reader.next?.recipient ?: break
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
_recipients.value = recipients
|
||||
.filter { !it.isGroupRecipient && it.hasApprovedMe() && it.address.serialize() != textSecurePreferences.getLocalNumber() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun filter(query: String): List<Recipient> {
|
||||
return _recipients.value?.filter {
|
||||
it.address.serialize().contains(query, ignoreCase = true) || it.name?.contains(query, ignoreCase = true) == true
|
||||
} ?: emptyList()
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
package org.thoughtcrime.securesms.groups
|
||||
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.google.android.material.chip.Chip
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.FragmentEnterCommunityUrlBinding
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupApi
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.util.State
|
||||
import org.thoughtcrime.securesms.util.hideKeyboard
|
||||
import java.util.Locale
|
||||
|
||||
class EnterCommunityUrlFragment : Fragment() {
|
||||
private lateinit var binding: FragmentEnterCommunityUrlBinding
|
||||
private val viewModel by activityViewModels<DefaultGroupsViewModel>()
|
||||
|
||||
var delegate: EnterCommunityUrlDelegate? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = FragmentEnterCommunityUrlBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.communityUrlEditText.imeOptions = binding.communityUrlEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||
binding.communityUrlEditText.addTextChangedListener { text -> binding.joinCommunityButton.isEnabled = !text.isNullOrBlank() }
|
||||
binding.communityUrlEditText.setOnFocusChangeListener { _, hasFocus -> binding.defaultRoomsContainer.isVisible = !hasFocus }
|
||||
binding.mainContainer.setOnTouchListener { _, _ ->
|
||||
binding.defaultRoomsContainer.isVisible = true
|
||||
binding.communityUrlEditText.clearFocus()
|
||||
binding.communityUrlEditText.hideKeyboard()
|
||||
true
|
||||
}
|
||||
binding.joinCommunityButton.setOnClickListener { joinCommunityIfPossible() }
|
||||
viewModel.defaultRooms.observe(viewLifecycleOwner) { state ->
|
||||
binding.defaultRoomsContainer.isVisible = state is State.Success
|
||||
binding.defaultRoomsLoaderContainer.isVisible = state is State.Loading
|
||||
binding.defaultRoomsLoader.isVisible = state is State.Loading
|
||||
when (state) {
|
||||
State.Loading -> {
|
||||
// TODO: Show a binding.loader
|
||||
}
|
||||
is State.Error -> {
|
||||
// TODO: Hide the binding.loader
|
||||
}
|
||||
is State.Success -> {
|
||||
populateDefaultGroups(state.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateDefaultGroups(groups: List<OpenGroupApi.DefaultGroup>) {
|
||||
binding.defaultRoomsFlexboxLayout.removeAllViews()
|
||||
groups.iterator().forEach { defaultGroup ->
|
||||
val chip = layoutInflater.inflate(R.layout.default_group_chip, binding.defaultRoomsFlexboxLayout, false) as Chip
|
||||
val drawable = defaultGroup.image?.let { bytes ->
|
||||
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
||||
RoundedBitmapDrawableFactory.create(resources, bitmap).apply {
|
||||
isCircular = true
|
||||
}
|
||||
}
|
||||
chip.chipIcon = drawable
|
||||
chip.text = defaultGroup.name
|
||||
chip.setOnClickListener {
|
||||
delegate?.handleCommunityUrlEntered(defaultGroup.joinURL)
|
||||
}
|
||||
binding.defaultRoomsFlexboxLayout.addView(chip)
|
||||
}
|
||||
}
|
||||
|
||||
// region Convenience
|
||||
private fun joinCommunityIfPossible() {
|
||||
val inputMethodManager = requireContext().getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(binding.communityUrlEditText.windowToken, 0)
|
||||
val communityUrl = binding.communityUrlEditText.text.trim().toString().lowercase(Locale.US)
|
||||
delegate?.handleCommunityUrlEntered(communityUrl)
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
fun interface EnterCommunityUrlDelegate {
|
||||
fun handleCommunityUrlEntered(url: String)
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
package org.thoughtcrime.securesms.groups
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.FragmentJoinCommunityBinding
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.OpenGroupUrlParser
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.conversation.start.NewConversationDelegate
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
|
||||
@AndroidEntryPoint
|
||||
class JoinCommunityFragment : Fragment() {
|
||||
|
||||
private lateinit var binding: FragmentJoinCommunityBinding
|
||||
|
||||
lateinit var delegate: NewConversationDelegate
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentJoinCommunityBinding.inflate(inflater)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.backButton.setOnClickListener { delegate.onDialogBackPressed() }
|
||||
binding.closeButton.setOnClickListener { delegate.onDialogClosePressed() }
|
||||
fun showLoader() {
|
||||
binding.loader.visibility = View.VISIBLE
|
||||
binding.loader.animate().setDuration(150).alpha(1.0f).start()
|
||||
}
|
||||
|
||||
fun hideLoader() {
|
||||
binding.loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() {
|
||||
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
super.onAnimationEnd(animation)
|
||||
binding.loader.visibility = View.GONE
|
||||
}
|
||||
})
|
||||
}
|
||||
fun joinCommunityIfPossible(url: String) {
|
||||
val openGroup = try {
|
||||
OpenGroupUrlParser.parseUrl(url)
|
||||
} catch (e: OpenGroupUrlParser.Error) {
|
||||
when (e) {
|
||||
is OpenGroupUrlParser.Error.MalformedURL -> return Toast.makeText(activity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
|
||||
is OpenGroupUrlParser.Error.InvalidPublicKey -> return Toast.makeText(activity, R.string.invalid_public_key, Toast.LENGTH_SHORT).show()
|
||||
is OpenGroupUrlParser.Error.NoPublicKey -> return Toast.makeText(activity, R.string.invalid_public_key, Toast.LENGTH_SHORT).show()
|
||||
is OpenGroupUrlParser.Error.NoRoom -> return Toast.makeText(activity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
showLoader()
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val sanitizedServer = openGroup.server.removeSuffix("/")
|
||||
val openGroupID = "$sanitizedServer.${openGroup.room}"
|
||||
OpenGroupManager.add(sanitizedServer, openGroup.room, openGroup.serverPublicKey, requireContext())
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
storage.onOpenGroupAdded(sanitizedServer)
|
||||
val threadID = GroupManager.getOpenGroupThreadID(openGroupID, requireContext())
|
||||
val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())
|
||||
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(requireContext())
|
||||
withContext(Dispatchers.Main) {
|
||||
val recipient = Recipient.from(requireContext(), Address.fromSerialized(groupID), false)
|
||||
openConversationActivity(requireContext(), threadID, recipient)
|
||||
delegate.onDialogClosePressed()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Couldn't join open group.", e)
|
||||
withContext(Dispatchers.Main) {
|
||||
hideLoader()
|
||||
Toast.makeText(activity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
}
|
||||
val urlDelegate = { url: String -> joinCommunityIfPossible(url) }
|
||||
binding.viewPager.adapter = JoinCommunityFragmentAdapter(
|
||||
parentFragment = this,
|
||||
enterCommunityUrlDelegate = urlDelegate,
|
||||
scanQrCodeDelegate = urlDelegate
|
||||
)
|
||||
val mediator = TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, pos ->
|
||||
tab.text = when (pos) {
|
||||
0 -> getString(R.string.activity_join_public_chat_enter_community_url_tab_title)
|
||||
1 -> getString(R.string.activity_join_public_chat_scan_qr_code_tab_title)
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
mediator.attach()
|
||||
}
|
||||
|
||||
private fun openConversationActivity(context: Context, threadId: Long, recipient: Recipient) {
|
||||
val intent = Intent(context, ConversationActivityV2::class.java)
|
||||
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId)
|
||||
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package org.thoughtcrime.securesms.groups
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
|
||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
|
||||
|
||||
class JoinCommunityFragmentAdapter(
|
||||
private val parentFragment: Fragment,
|
||||
private val enterCommunityUrlDelegate: EnterCommunityUrlDelegate,
|
||||
private val scanQrCodeDelegate: ScanQRCodeWrapperFragmentDelegate
|
||||
) : FragmentStateAdapter(parentFragment) {
|
||||
|
||||
override fun getItemCount(): Int = 2
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return when (position) {
|
||||
0 -> EnterCommunityUrlFragment().apply { delegate = enterCommunityUrlDelegate }
|
||||
1 -> ScanQRCodeWrapperFragment().apply { delegate = scanQrCodeDelegate }
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,229 +0,0 @@
|
||||
package org.thoughtcrime.securesms.groups
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.chip.Chip
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import network.loki.messenger.R
|
||||
import network.loki.messenger.databinding.ActivityJoinPublicChatBinding
|
||||
import network.loki.messenger.databinding.FragmentEnterChatUrlBinding
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.messaging.open_groups.OpenGroupApi.DefaultGroup
|
||||
import org.session.libsession.utilities.Address
|
||||
import org.session.libsession.utilities.GroupUtil
|
||||
import org.session.libsession.utilities.OpenGroupUrlParser
|
||||
import org.session.libsession.utilities.OpenGroupUrlParser.Error
|
||||
import org.session.libsession.utilities.recipients.Recipient
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
|
||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
|
||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
|
||||
import org.thoughtcrime.securesms.util.State
|
||||
import java.util.Locale
|
||||
|
||||
class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCodeWrapperFragmentDelegate {
|
||||
private lateinit var binding: ActivityJoinPublicChatBinding
|
||||
private val adapter = JoinPublicChatActivityAdapter(this)
|
||||
|
||||
// region Lifecycle
|
||||
override fun onCreate(savedInstanceState: Bundle?, isReady: Boolean) {
|
||||
super.onCreate(savedInstanceState, isReady)
|
||||
binding = ActivityJoinPublicChatBinding.inflate(layoutInflater)
|
||||
// Set content view
|
||||
setContentView(binding.root)
|
||||
// Set title
|
||||
supportActionBar!!.title = resources.getString(R.string.activity_join_public_chat_title)
|
||||
// Set up view pager
|
||||
binding.viewPager.adapter = adapter
|
||||
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Updating
|
||||
private fun showLoader() {
|
||||
binding.loader.visibility = View.VISIBLE
|
||||
binding.loader.animate().setDuration(150).alpha(1.0f).start()
|
||||
}
|
||||
|
||||
private fun hideLoader() {
|
||||
binding.loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() {
|
||||
|
||||
override fun onAnimationEnd(animation: Animator?) {
|
||||
super.onAnimationEnd(animation)
|
||||
binding.loader.visibility = View.GONE
|
||||
}
|
||||
})
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
override fun handleQRCodeScanned(url: String) {
|
||||
joinPublicChatIfPossible(url)
|
||||
}
|
||||
|
||||
fun joinPublicChatIfPossible(url: String) {
|
||||
// Add "http" if not entered explicitly
|
||||
val openGroup = try {
|
||||
OpenGroupUrlParser.parseUrl(url)
|
||||
} catch (e: Error) {
|
||||
when (e) {
|
||||
is Error.MalformedURL -> return Toast.makeText(this, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
|
||||
is Error.InvalidPublicKey -> return Toast.makeText(this, R.string.invalid_public_key, Toast.LENGTH_SHORT).show()
|
||||
is Error.NoPublicKey -> return Toast.makeText(this, R.string.invalid_public_key, Toast.LENGTH_SHORT).show()
|
||||
is Error.NoRoom -> return Toast.makeText(this, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
showLoader()
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val sanitizedServer = openGroup.server.removeSuffix("/")
|
||||
val openGroupID = "$sanitizedServer.${openGroup.room}"
|
||||
OpenGroupManager.add(sanitizedServer, openGroup.room, openGroup.serverPublicKey, this@JoinPublicChatActivity)
|
||||
val storage = MessagingModuleConfiguration.shared.storage
|
||||
storage.onOpenGroupAdded(sanitizedServer)
|
||||
val threadID = GroupManager.getOpenGroupThreadID(openGroupID, this@JoinPublicChatActivity)
|
||||
val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray())
|
||||
|
||||
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@JoinPublicChatActivity)
|
||||
withContext(Dispatchers.Main) {
|
||||
val recipient = Recipient.from(this@JoinPublicChatActivity, Address.fromSerialized(groupID), false)
|
||||
openConversationActivity(this@JoinPublicChatActivity, threadID, recipient)
|
||||
finish()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("Loki", "Couldn't join open group.", e)
|
||||
withContext(Dispatchers.Main) {
|
||||
hideLoader()
|
||||
Toast.makeText(this@JoinPublicChatActivity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Convenience
|
||||
private fun openConversationActivity(context: Context, threadId: Long, recipient: Recipient) {
|
||||
val intent = Intent(context, ConversationActivityV2::class.java)
|
||||
intent.putExtra(ConversationActivityV2.THREAD_ID, threadId)
|
||||
intent.putExtra(ConversationActivityV2.ADDRESS, recipient.address)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
||||
// region Adapter
|
||||
private class JoinPublicChatActivityAdapter(val activity: JoinPublicChatActivity) : FragmentPagerAdapter(activity.supportFragmentManager) {
|
||||
|
||||
override fun getCount(): Int {
|
||||
return 2
|
||||
}
|
||||
|
||||
override fun getItem(index: Int): Fragment {
|
||||
return when (index) {
|
||||
0 -> EnterChatURLFragment()
|
||||
1 -> {
|
||||
val result = ScanQRCodeWrapperFragment()
|
||||
result.delegate = activity
|
||||
result.message = activity.resources.getString(R.string.activity_join_public_chat_scan_qr_code_explanation)
|
||||
result
|
||||
}
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPageTitle(index: Int): CharSequence {
|
||||
return when (index) {
|
||||
0 -> activity.resources.getString(R.string.activity_join_public_chat_enter_group_url_tab_title)
|
||||
1 -> activity.resources.getString(R.string.activity_join_public_chat_scan_qr_code_tab_title)
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Enter Chat URL Fragment
|
||||
class EnterChatURLFragment : Fragment() {
|
||||
private lateinit var binding: FragmentEnterChatUrlBinding
|
||||
private val viewModel by activityViewModels<DefaultGroupsViewModel>()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = FragmentEnterChatUrlBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.chatURLEditText.imeOptions = binding.chatURLEditText.imeOptions or 16777216 // Always use incognito keyboard
|
||||
binding.joinPublicChatButton.setOnClickListener { joinPublicChatIfPossible() }
|
||||
viewModel.defaultRooms.observe(viewLifecycleOwner) { state ->
|
||||
binding.defaultRoomsContainer.isVisible = state is State.Success
|
||||
binding.defaultRoomsLoaderContainer.isVisible = state is State.Loading
|
||||
binding.defaultRoomsLoader.isVisible = state is State.Loading
|
||||
when (state) {
|
||||
State.Loading -> {
|
||||
// TODO: Show a binding.loader
|
||||
}
|
||||
is State.Error -> {
|
||||
// TODO: Hide the binding.loader
|
||||
}
|
||||
is State.Success -> {
|
||||
populateDefaultGroups(state.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun populateDefaultGroups(groups: List<DefaultGroup>) {
|
||||
binding.defaultRoomsGridLayout.removeAllViews()
|
||||
binding.defaultRoomsGridLayout.useDefaultMargins = false
|
||||
groups.iterator().forEach { defaultGroup ->
|
||||
val chip = layoutInflater.inflate(R.layout.default_group_chip, binding.defaultRoomsGridLayout, false) as Chip
|
||||
val drawable = defaultGroup.image?.let { bytes ->
|
||||
val bitmap = BitmapFactory.decodeByteArray(bytes,0, bytes.size)
|
||||
RoundedBitmapDrawableFactory.create(resources,bitmap).apply {
|
||||
isCircular = true
|
||||
}
|
||||
}
|
||||
chip.chipIcon = drawable
|
||||
chip.text = defaultGroup.name
|
||||
chip.setOnClickListener {
|
||||
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(defaultGroup.joinURL)
|
||||
}
|
||||
binding.defaultRoomsGridLayout.addView(chip)
|
||||
}
|
||||
if ((groups.size and 1) != 0) { // This checks that the number of rooms is even
|
||||
layoutInflater.inflate(R.layout.grid_layout_filler, binding.defaultRoomsGridLayout)
|
||||
}
|
||||
}
|
||||
|
||||
// region Convenience
|
||||
private fun joinPublicChatIfPossible() {
|
||||
val inputMethodManager = requireContext().getSystemService(BaseActionBarActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputMethodManager.hideSoftInputFromWindow(binding.chatURLEditText.windowToken, 0)
|
||||
val chatURL = binding.chatURLEditText.text.trim().toString().toLowerCase(Locale.US)
|
||||
(requireActivity() as JoinPublicChatActivity).joinPublicChatIfPossible(chatURL)
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
// endregion
|
@ -41,6 +41,7 @@ import org.session.libsignal.utilities.toHexString
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.MuteDialog
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||
import org.thoughtcrime.securesms.conversation.start.NewConversationFragment
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
import org.thoughtcrime.securesms.conversation.v2.utilities.NotificationUtils
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
|
||||
@ -50,9 +51,6 @@ import org.thoughtcrime.securesms.database.RecipientDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||
import org.thoughtcrime.securesms.dms.CreatePrivateChatActivity
|
||||
import org.thoughtcrime.securesms.groups.CreateClosedGroupActivity
|
||||
import org.thoughtcrime.securesms.groups.JoinPublicChatActivity
|
||||
import org.thoughtcrime.securesms.groups.OpenGroupManager
|
||||
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter
|
||||
import org.thoughtcrime.securesms.home.search.GlobalSearchInputLayout
|
||||
@ -78,7 +76,6 @@ import javax.inject.Inject
|
||||
class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
ConversationClickListener,
|
||||
SeedReminderViewDelegate,
|
||||
NewConversationButtonSetViewDelegate,
|
||||
GlobalSearchInputLayout.GlobalSearchInputLayoutListener {
|
||||
|
||||
private lateinit var binding: ActivityHomeBinding
|
||||
@ -178,7 +175,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
binding.recyclerView.adapter = homeAdapter
|
||||
binding.globalSearchRecycler.adapter = globalSearchAdapter
|
||||
// Set up empty state view
|
||||
binding.createNewPrivateChatButton.setOnClickListener { createNewPrivateChat() }
|
||||
binding.createNewPrivateChatButton.setOnClickListener { showNewConversation() }
|
||||
IP2Country.configureIfNeeded(this@HomeActivity)
|
||||
homeViewModel.getObservable(this).observe(this) { newData ->
|
||||
val manager = binding.recyclerView.layoutManager as LinearLayoutManager
|
||||
@ -194,8 +191,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
updateEmptyState()
|
||||
}
|
||||
homeViewModel.tryUpdateChannel()
|
||||
// Set up new conversation button set
|
||||
binding.newConversationButtonSet.delegate = this
|
||||
// Set up new conversation button
|
||||
binding.newConversationButton.setOnClickListener { showNewConversation() }
|
||||
// Observe blocked contacts changed events
|
||||
val broadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
@ -301,7 +298,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
binding.seedReminderView.isVisible = !TextSecurePreferences.getHasViewedSeed(this) && !isShown
|
||||
binding.gradientView.isVisible = !isShown
|
||||
binding.globalSearchRecycler.isVisible = isShown
|
||||
binding.newConversationButtonSet.isVisible = !isShown
|
||||
binding.newConversationButton.isVisible = !isShown
|
||||
}
|
||||
|
||||
private fun setupMessageRequestsBanner() {
|
||||
@ -354,13 +351,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(false)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (resultCode == CreateClosedGroupActivity.closedGroupCreatedResultCode) {
|
||||
createNewPrivateChat()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
val broadcastReceiver = this.broadcastReceiver
|
||||
if (broadcastReceiver != null) {
|
||||
@ -623,19 +613,9 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
||||
.create().show()
|
||||
}
|
||||
|
||||
override fun createNewPrivateChat() {
|
||||
val intent = Intent(this, CreatePrivateChatActivity::class.java)
|
||||
show(intent)
|
||||
private fun showNewConversation() {
|
||||
NewConversationFragment().show(supportFragmentManager, "NewConversationFragment")
|
||||
}
|
||||
|
||||
override fun createNewClosedGroup() {
|
||||
val intent = Intent(this, CreateClosedGroupActivity::class.java)
|
||||
show(intent, true)
|
||||
}
|
||||
|
||||
override fun joinOpenGroup() {
|
||||
val intent = Intent(this, JoinPublicChatActivity::class.java)
|
||||
show(intent)
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
|
@ -1,386 +0,0 @@
|
||||
package org.thoughtcrime.securesms.home
|
||||
|
||||
import android.animation.FloatEvaluator
|
||||
import android.animation.PointFEvaluator
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.PointF
|
||||
import android.graphics.Typeface
|
||||
import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.annotation.DrawableRes
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
||||
import org.thoughtcrime.securesms.util.NewConversationButtonImageView
|
||||
import org.thoughtcrime.securesms.util.UiModeUtilities
|
||||
import org.thoughtcrime.securesms.util.animateSizeChange
|
||||
import org.thoughtcrime.securesms.util.contains
|
||||
import org.thoughtcrime.securesms.util.disableClipping
|
||||
import org.thoughtcrime.securesms.util.distanceTo
|
||||
import org.thoughtcrime.securesms.util.getColorWithID
|
||||
import org.thoughtcrime.securesms.util.isAbove
|
||||
import org.thoughtcrime.securesms.util.isLeftOf
|
||||
import org.thoughtcrime.securesms.util.isRightOf
|
||||
import org.thoughtcrime.securesms.util.toPx
|
||||
|
||||
class NewConversationButtonSetView : RelativeLayout {
|
||||
private var expandedButton: Button? = null
|
||||
private var previousAction: Int? = null
|
||||
private var isExpanded = false
|
||||
var delegate: NewConversationButtonSetViewDelegate? = null
|
||||
|
||||
// region Convenience
|
||||
private val sessionButtonExpandedPosition: PointF get() { return PointF(width.toFloat() / 2 - sessionButton.expandedSize / 2 - sessionButton.shadowMargin, 0.0f) }
|
||||
private val sessionTooltipExpandedPosition: PointF get() {
|
||||
val x = sessionButtonExpandedPosition.x + sessionButton.width / 2 - sessionTooltip.width / 2
|
||||
val y = sessionButtonExpandedPosition.y + sessionButton.height - sessionTooltip.height / 2
|
||||
return PointF(x, y)
|
||||
}
|
||||
private val closedGroupButtonExpandedPosition: PointF get() { return PointF(width.toFloat() - closedGroupButton.expandedSize - 2 * closedGroupButton.shadowMargin, height.toFloat() - bottomMargin - closedGroupButton.expandedSize - 2 * closedGroupButton.shadowMargin) }
|
||||
private val closedGroupTooltipExpandedPosition: PointF get() {
|
||||
val x = closedGroupButtonExpandedPosition.x + closedGroupButton.width / 2 - closedGroupTooltip.width / 2
|
||||
val y = closedGroupButtonExpandedPosition.y + closedGroupButton.height - closedGroupTooltip.height / 2
|
||||
return PointF(x, y)
|
||||
}
|
||||
private val openGroupButtonExpandedPosition: PointF get() { return PointF(0.0f, height.toFloat() - bottomMargin - openGroupButton.expandedSize - 2 * openGroupButton.shadowMargin) }
|
||||
private val openGroupTooltipExpandedPosition: PointF get() {
|
||||
val x = openGroupButtonExpandedPosition.x + openGroupButton.width / 2 - openGroupTooltip.width / 2
|
||||
val y = openGroupButtonExpandedPosition.y + openGroupButton.height - openGroupTooltip.height / 2
|
||||
return PointF(x, y)
|
||||
}
|
||||
private val buttonRestPosition: PointF get() { return PointF(width.toFloat() / 2 - mainButton.expandedSize / 2 - mainButton.shadowMargin, height.toFloat() - bottomMargin - mainButton.expandedSize - 2 * mainButton.shadowMargin) }
|
||||
private fun tooltipRestPosition(viewWidth: Int): PointF {
|
||||
return PointF(width.toFloat() / 2 - viewWidth / 2, height.toFloat() - bottomMargin)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Settings
|
||||
private val minDragDistance by lazy { toPx(40, resources).toFloat() }
|
||||
private val maxDragDistance by lazy { toPx(56, resources).toFloat() }
|
||||
private val dragMargin by lazy { toPx(16, resources).toFloat() }
|
||||
private val bottomMargin by lazy { resources.getDimension(R.dimen.new_conversation_button_bottom_offset) }
|
||||
// endregion
|
||||
|
||||
// region Components
|
||||
private val mainButton by lazy { Button(context, true, R.drawable.ic_plus) }
|
||||
private val sessionButton by lazy { Button(context, false, R.drawable.ic_message) }
|
||||
private val sessionTooltip by lazy {
|
||||
TextView(context).apply {
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
setText(R.string.NewConversationButton_SessionTooltip)
|
||||
isAllCaps = true
|
||||
}
|
||||
}
|
||||
private val closedGroupButton by lazy { Button(context, false, R.drawable.ic_group) }
|
||||
private val closedGroupTooltip by lazy {
|
||||
TextView(context).apply {
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
setText(R.string.NewConversationButton_ClosedGroupTooltip)
|
||||
isAllCaps = true
|
||||
}
|
||||
}
|
||||
private val openGroupButton by lazy { Button(context, false, R.drawable.ic_globe) }
|
||||
private val openGroupTooltip by lazy {
|
||||
TextView(context).apply {
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
setText(R.string.NewConversationButton_OpenGroupTooltip)
|
||||
isAllCaps = true
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Button
|
||||
class Button : RelativeLayout {
|
||||
@DrawableRes private var iconID = 0
|
||||
private var isMain = false
|
||||
|
||||
fun getIconID() = iconID
|
||||
|
||||
companion object {
|
||||
val animationDuration = 250.toLong()
|
||||
}
|
||||
|
||||
val expandedSize by lazy { resources.getDimension(R.dimen.new_conversation_button_expanded_size) }
|
||||
val collapsedSize by lazy { resources.getDimension(R.dimen.new_conversation_button_collapsed_size) }
|
||||
val shadowMargin by lazy { toPx(6, resources).toFloat() }
|
||||
private val expandedImageViewPosition by lazy { PointF(shadowMargin, shadowMargin) }
|
||||
private val collapsedImageViewPosition by lazy { PointF(shadowMargin + (expandedSize - collapsedSize) / 2, shadowMargin + (expandedSize - collapsedSize) / 2) }
|
||||
|
||||
private val imageView by lazy {
|
||||
val result = NewConversationButtonImageView(context)
|
||||
val size = collapsedSize.toInt()
|
||||
result.layoutParams = LayoutParams(size, size)
|
||||
result.setBackgroundResource(R.drawable.new_conversation_button_background)
|
||||
@ColorRes val backgroundColorID = if (isMain) R.color.accent else R.color.new_conversation_button_collapsed_background
|
||||
@ColorRes val shadowColorID = if (isMain) {
|
||||
R.color.new_conversation_button_shadow
|
||||
} else {
|
||||
if (UiModeUtilities.isDayUiMode(context)) R.color.transparent_black_30 else R.color.black
|
||||
}
|
||||
result.mainColor = resources.getColorWithID(backgroundColorID, context.theme)
|
||||
result.sessionShadowColor = resources.getColorWithID(shadowColorID, context.theme)
|
||||
result.scaleType = ImageView.ScaleType.CENTER
|
||||
result.setImageResource(iconID)
|
||||
result.imageTintList = if (isMain) {
|
||||
ColorStateList.valueOf(resources.getColorWithID(android.R.color.white, context.theme))
|
||||
} else {
|
||||
ColorStateList.valueOf(resources.getColorWithID(R.color.text, context.theme))
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
constructor(context: Context) : super(context) { throw IllegalAccessException("Use Button(context:iconID:) instead.") }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { throw IllegalAccessException("Use Button(context:iconID:) instead.") }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { throw IllegalAccessException("Use Button(context:iconID:) instead.") }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { throw IllegalAccessException("Use Button(context:iconID:) instead.") }
|
||||
|
||||
constructor(context: Context, isMain: Boolean, @DrawableRes iconID: Int) : super(context) {
|
||||
this.iconID = iconID
|
||||
this.isMain = isMain
|
||||
disableClipping()
|
||||
val size = resources.getDimension(R.dimen.new_conversation_button_expanded_size).toInt() + 2 * shadowMargin.toInt()
|
||||
val layoutParams = LayoutParams(size, size)
|
||||
this.layoutParams = layoutParams
|
||||
addView(imageView)
|
||||
imageView.x = collapsedImageViewPosition.x
|
||||
imageView.y = collapsedImageViewPosition.y
|
||||
gravity = Gravity.TOP or Gravity.LEFT // Intentionally not Gravity.START
|
||||
}
|
||||
|
||||
fun expand() {
|
||||
GlowViewUtilities.animateColorChange(context, imageView, R.color.new_conversation_button_collapsed_background, R.color.accent)
|
||||
@ColorRes val startShadowColorID = if (UiModeUtilities.isDayUiMode(context)) R.color.transparent_black_30 else R.color.black
|
||||
GlowViewUtilities.animateShadowColorChange(context, imageView, startShadowColorID, R.color.new_conversation_button_shadow)
|
||||
imageView.animateSizeChange(R.dimen.new_conversation_button_collapsed_size, R.dimen.new_conversation_button_expanded_size, animationDuration)
|
||||
animateImageViewPositionChange(collapsedImageViewPosition, expandedImageViewPosition)
|
||||
}
|
||||
|
||||
fun collapse() {
|
||||
GlowViewUtilities.animateColorChange(context, imageView, R.color.accent, R.color.new_conversation_button_collapsed_background)
|
||||
@ColorRes val endShadowColorID = if (UiModeUtilities.isDayUiMode(context)) R.color.transparent_black_30 else R.color.black
|
||||
GlowViewUtilities.animateShadowColorChange(context, imageView, R.color.new_conversation_button_shadow, endShadowColorID)
|
||||
imageView.animateSizeChange(R.dimen.new_conversation_button_expanded_size, R.dimen.new_conversation_button_collapsed_size, animationDuration)
|
||||
animateImageViewPositionChange(expandedImageViewPosition, collapsedImageViewPosition)
|
||||
}
|
||||
|
||||
private fun animateImageViewPositionChange(startPosition: PointF, endPosition: PointF) {
|
||||
val animation = ValueAnimator.ofObject(PointFEvaluator(), startPosition, endPosition)
|
||||
animation.duration = animationDuration
|
||||
animation.addUpdateListener { animator ->
|
||||
val point = animator.animatedValue as PointF
|
||||
imageView.x = point.x
|
||||
imageView.y = point.y
|
||||
}
|
||||
animation.start()
|
||||
}
|
||||
|
||||
fun animatePositionChange(startPosition: PointF, endPosition: PointF) {
|
||||
val animation = ValueAnimator.ofObject(PointFEvaluator(), startPosition, endPosition)
|
||||
animation.duration = animationDuration
|
||||
animation.addUpdateListener { animator ->
|
||||
val point = animator.animatedValue as PointF
|
||||
x = point.x
|
||||
y = point.y
|
||||
}
|
||||
animation.start()
|
||||
}
|
||||
|
||||
fun animateAlphaChange(startAlpha: Float, endAlpha: Float) {
|
||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), startAlpha, endAlpha)
|
||||
animation.duration = animationDuration
|
||||
animation.addUpdateListener { animator ->
|
||||
alpha = animator.animatedValue as Float
|
||||
}
|
||||
animation.start()
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Lifecycle
|
||||
constructor(context: Context) : super(context) { setUpViewHierarchy() }
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { setUpViewHierarchy() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { setUpViewHierarchy() }
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { setUpViewHierarchy() }
|
||||
|
||||
private fun setUpViewHierarchy() {
|
||||
disableClipping()
|
||||
isHapticFeedbackEnabled = true
|
||||
// Set up session button
|
||||
addView(sessionButton)
|
||||
addView(sessionTooltip)
|
||||
sessionButton.alpha = 0.0f
|
||||
sessionTooltip.alpha = 0.0f
|
||||
val sessionButtonLayoutParams = sessionButton.layoutParams as LayoutParams
|
||||
sessionButtonLayoutParams.addRule(CENTER_IN_PARENT, TRUE)
|
||||
sessionButtonLayoutParams.addRule(ALIGN_PARENT_BOTTOM, TRUE)
|
||||
sessionButtonLayoutParams.bottomMargin = bottomMargin.toInt()
|
||||
// Set up closed group button
|
||||
addView(closedGroupButton)
|
||||
addView(closedGroupTooltip)
|
||||
closedGroupButton.alpha = 0.0f
|
||||
closedGroupTooltip.alpha = 0.0f
|
||||
val closedGroupButtonLayoutParams = closedGroupButton.layoutParams as LayoutParams
|
||||
closedGroupButtonLayoutParams.addRule(CENTER_IN_PARENT, TRUE)
|
||||
closedGroupButtonLayoutParams.addRule(ALIGN_PARENT_BOTTOM, TRUE)
|
||||
closedGroupButtonLayoutParams.bottomMargin = bottomMargin.toInt()
|
||||
// Set up open group button
|
||||
addView(openGroupButton)
|
||||
addView(openGroupTooltip)
|
||||
openGroupButton.alpha = 0.0f
|
||||
openGroupTooltip.alpha = 0.0f
|
||||
val openGroupButtonLayoutParams = openGroupButton.layoutParams as LayoutParams
|
||||
openGroupButtonLayoutParams.addRule(CENTER_IN_PARENT, TRUE)
|
||||
openGroupButtonLayoutParams.addRule(ALIGN_PARENT_BOTTOM, TRUE)
|
||||
openGroupButtonLayoutParams.bottomMargin = bottomMargin.toInt()
|
||||
// Set up main button
|
||||
addView(mainButton)
|
||||
val mainButtonLayoutParams = mainButton.layoutParams as LayoutParams
|
||||
mainButtonLayoutParams.addRule(CENTER_IN_PARENT, TRUE)
|
||||
mainButtonLayoutParams.addRule(ALIGN_PARENT_BOTTOM, TRUE)
|
||||
mainButtonLayoutParams.bottomMargin = bottomMargin.toInt()
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region Interaction
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
val touch = PointF(event.x, event.y)
|
||||
val allButtons = listOf( mainButton, sessionButton, closedGroupButton, openGroupButton )
|
||||
val buttonsExcludingMainButton = listOf( sessionButton, closedGroupButton, openGroupButton )
|
||||
if (allButtons.none { it.contains(touch) }) { return false }
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
if (isExpanded) {
|
||||
if (mainButton.contains(touch)) { collapse() }
|
||||
} else {
|
||||
isExpanded = true
|
||||
expand()
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK)
|
||||
} else {
|
||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
mainButton.x = touch.x - mainButton.expandedSize / 2
|
||||
mainButton.y = touch.y - mainButton.expandedSize / 2
|
||||
mainButton.alpha = 1 - (PointF(mainButton.x, mainButton.y).distanceTo(buttonRestPosition) / maxDragDistance)
|
||||
val buttonToExpand = buttonsExcludingMainButton.firstOrNull { button ->
|
||||
var hasUserDraggedBeyondButton = false
|
||||
if (button == openGroupButton && touch.isLeftOf(openGroupButton, dragMargin)) { hasUserDraggedBeyondButton = true }
|
||||
if (button == sessionButton && touch.isAbove(sessionButton, dragMargin)) { hasUserDraggedBeyondButton = true }
|
||||
if (button == closedGroupButton && touch.isRightOf(closedGroupButton, dragMargin)) { hasUserDraggedBeyondButton = true }
|
||||
button.contains(touch) || hasUserDraggedBeyondButton
|
||||
}
|
||||
if (buttonToExpand != null) {
|
||||
if (buttonToExpand == expandedButton) { return true }
|
||||
expandedButton?.collapse()
|
||||
buttonToExpand.expand()
|
||||
this.expandedButton = buttonToExpand
|
||||
} else {
|
||||
expandedButton?.collapse()
|
||||
this.expandedButton = null
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
val mainButtonCenter = PointF(width.toFloat() / 2, height.toFloat() - bottomMargin - mainButton.expandedSize / 2)
|
||||
val distanceFromMainButtonCenter = touch.distanceTo(mainButtonCenter)
|
||||
fun collapse() {
|
||||
isExpanded = false
|
||||
this.collapse()
|
||||
}
|
||||
if (distanceFromMainButtonCenter > (minDragDistance + mainButton.collapsedSize / 2)) {
|
||||
if (sessionButton.contains(touch) || touch.isAbove(sessionButton, dragMargin)) { delegate?.createNewPrivateChat(); collapse() }
|
||||
else if (closedGroupButton.contains(touch) || touch.isRightOf(closedGroupButton, dragMargin)) { delegate?.createNewClosedGroup(); collapse() }
|
||||
else if (openGroupButton.contains(touch) || touch.isLeftOf(openGroupButton, dragMargin)) { delegate?.joinOpenGroup(); collapse() }
|
||||
else { collapse() }
|
||||
} else {
|
||||
val currentPosition = PointF(mainButton.x, mainButton.y)
|
||||
mainButton.animatePositionChange(currentPosition, buttonRestPosition)
|
||||
val endAlpha = 1.0f
|
||||
mainButton.animateAlphaChange(mainButton.alpha, endAlpha)
|
||||
expandedButton?.collapse()
|
||||
this.expandedButton = null
|
||||
}
|
||||
}
|
||||
}
|
||||
previousAction = event.action
|
||||
return true
|
||||
}
|
||||
|
||||
private fun expand() {
|
||||
val buttonsExcludingMainButton = listOf( sessionButton, closedGroupButton, openGroupButton )
|
||||
val allTooltips = listOf(sessionTooltip, closedGroupTooltip, openGroupTooltip)
|
||||
|
||||
sessionButton.animatePositionChange(buttonRestPosition, sessionButtonExpandedPosition)
|
||||
sessionTooltip.animatePositionChange(tooltipRestPosition(sessionTooltip.width), sessionTooltipExpandedPosition)
|
||||
closedGroupButton.animatePositionChange(buttonRestPosition, closedGroupButtonExpandedPosition)
|
||||
closedGroupTooltip.animatePositionChange(tooltipRestPosition(closedGroupTooltip.width), closedGroupTooltipExpandedPosition)
|
||||
openGroupButton.animatePositionChange(buttonRestPosition, openGroupButtonExpandedPosition)
|
||||
openGroupTooltip.animatePositionChange(tooltipRestPosition(openGroupTooltip.width), openGroupTooltipExpandedPosition)
|
||||
buttonsExcludingMainButton.forEach { it.animateAlphaChange(0.0f, 1.0f) }
|
||||
allTooltips.forEach { it.animateAlphaChange(0.0f, 1.0f) }
|
||||
postDelayed({ isExpanded = true }, Button.animationDuration)
|
||||
}
|
||||
|
||||
private fun collapse() {
|
||||
val allButtons = listOf( mainButton, sessionButton, closedGroupButton, openGroupButton )
|
||||
val allTooltips = listOf(sessionTooltip, closedGroupTooltip, openGroupTooltip)
|
||||
|
||||
allButtons.forEach {
|
||||
val currentPosition = PointF(it.x, it.y)
|
||||
it.animatePositionChange(currentPosition, buttonRestPosition)
|
||||
val endAlpha = if (it == mainButton) 1.0f else 0.0f
|
||||
it.animateAlphaChange(it.alpha, endAlpha)
|
||||
}
|
||||
allTooltips.forEach {
|
||||
it.animateAlphaChange(1.0f, 0.0f)
|
||||
it.animatePositionChange(PointF(it.x, it.y), tooltipRestPosition(it.width))
|
||||
}
|
||||
postDelayed({ isExpanded = false }, Button.animationDuration)
|
||||
}
|
||||
// endregion
|
||||
|
||||
fun View.animatePositionChange(startPosition: PointF, endPosition: PointF) {
|
||||
val animation = ValueAnimator.ofObject(PointFEvaluator(), startPosition, endPosition)
|
||||
animation.duration = Button.animationDuration
|
||||
animation.addUpdateListener { animator ->
|
||||
val point = animator.animatedValue as PointF
|
||||
x = point.x
|
||||
y = point.y
|
||||
}
|
||||
animation.start()
|
||||
}
|
||||
|
||||
fun View.animateAlphaChange(startAlpha: Float, endAlpha: Float) {
|
||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), startAlpha, endAlpha)
|
||||
animation.duration = Button.animationDuration
|
||||
animation.addUpdateListener { animator ->
|
||||
alpha = animator.animatedValue as Float
|
||||
}
|
||||
animation.start()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// region Delegate
|
||||
interface NewConversationButtonSetViewDelegate {
|
||||
|
||||
fun joinOpenGroup()
|
||||
fun createNewPrivateChat()
|
||||
fun createNewClosedGroup()
|
||||
}
|
||||
// endregion
|
@ -6,6 +6,7 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import network.loki.messenger.databinding.FragmentScanQrCodeBinding
|
||||
import org.thoughtcrime.securesms.qr.ScanListener
|
||||
@ -30,6 +31,7 @@ class ScanQRCodeFragment : Fragment() {
|
||||
else -> binding.overlayView.orientation = LinearLayout.VERTICAL
|
||||
}
|
||||
binding.messageTextView.text = message
|
||||
binding.messageTextView.isVisible = message.isNotEmpty()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
@ -11,7 +11,6 @@ import androidx.fragment.app.Fragment
|
||||
import com.tbruyelle.rxpermissions2.RxPermissions
|
||||
import network.loki.messenger.R
|
||||
import org.thoughtcrime.securesms.qr.ScanListener
|
||||
import org.thoughtcrime.securesms.util.ScanQRCodeFragment
|
||||
|
||||
class ScanQRCodeWrapperFragment : Fragment(), ScanQRCodePlaceholderFragmentDelegate, ScanListener {
|
||||
|
||||
@ -80,14 +79,14 @@ class ScanQRCodeWrapperFragment : Fragment(), ScanQRCodePlaceholderFragmentDeleg
|
||||
}
|
||||
}
|
||||
|
||||
override fun onQrDataFound(string: String) {
|
||||
override fun onQrDataFound(data: String) {
|
||||
activity?.runOnUiThread {
|
||||
delegate?.handleQRCodeScanned(string)
|
||||
delegate?.handleQRCodeScanned(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ScanQRCodeWrapperFragmentDelegate {
|
||||
fun interface ScanQRCodeWrapperFragmentDelegate {
|
||||
|
||||
fun handleQRCodeScanned(string: String)
|
||||
}
|
@ -4,10 +4,12 @@ import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.FloatEvaluator
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.graphics.PointF
|
||||
import android.graphics.Rect
|
||||
import androidx.annotation.DimenRes
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
|
||||
fun View.contains(point: PointF): Boolean {
|
||||
return hitRect.contains(point.x.toInt(), point.y.toInt())
|
||||
@ -52,3 +54,8 @@ fun View.fadeOut(duration: Long = 150) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun View.hideKeyboard() {
|
||||
val imm = this.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(this.windowToken, 0)
|
||||
}
|
||||
|
10
app/src/main/res/drawable/ic_baseline_close_24.xml
Normal file
10
app/src/main/res/drawable/ic_baseline_close_24.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
|
||||
</vector>
|
@ -1,88 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/contentView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:background="?android:dividerHorizontal"
|
||||
android:elevation="1dp" />
|
||||
|
||||
<EditText
|
||||
style="@style/SmallSessionEditText"
|
||||
android:id="@+id/chatURLEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginLeft="@dimen/large_spacing"
|
||||
android:layout_marginTop="@dimen/large_spacing"
|
||||
android:layout_marginRight="@dimen/large_spacing"
|
||||
android:inputType="textWebEmailAddress|textMultiLine"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingBottom="0dp"
|
||||
android:gravity="center_vertical"
|
||||
android:maxLines="3"
|
||||
android:hint="@string/fragment_enter_chat_url_edit_text_hint" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/defaultRoomsLoaderContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:layout_marginTop="56dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<com.github.ybq.android.spinkit.SpinKitView
|
||||
android:id="@+id/defaultRoomsLoader"
|
||||
style="@style/SpinKitView.Large.ThreeBounce"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:SpinKit_Color="@color/text"
|
||||
android:visibility="gone"
|
||||
android:layout_centerInParent="true" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:visibility="gone"
|
||||
android:id="@+id/defaultRoomsContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_marginVertical="16dp"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:paddingHorizontal="24dp"
|
||||
android:text="@string/activity_join_public_chat_join_rooms"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<GridLayout
|
||||
android:id="@+id/defaultRoomsGridLayout"
|
||||
android:columnCount="2"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.ProminentOutline"
|
||||
android:id="@+id/joinPublicChatButton"
|
||||
android:layout_width="196dp"
|
||||
android:layout_height="@dimen/medium_button_height"
|
||||
android:layout_marginBottom="@dimen/medium_spacing"
|
||||
android:text="@string/next" />
|
||||
|
||||
</LinearLayout>
|
@ -1,115 +0,0 @@
|
||||
<?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:id="@+id/contentView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:background="?android:dividerHorizontal"
|
||||
android:elevation="1dp" />
|
||||
|
||||
<EditText
|
||||
style="@style/SmallSessionEditText"
|
||||
android:id="@+id/publicKeyEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginLeft="@dimen/large_spacing"
|
||||
android:layout_marginTop="@dimen/large_spacing"
|
||||
android:layout_marginRight="@dimen/large_spacing"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingBottom="0dp"
|
||||
android:gravity="center_vertical"
|
||||
android:inputType="textMultiLine"
|
||||
android:maxLines="3"
|
||||
android:imeOptions="actionDone"
|
||||
android:hint="@string/fragment_enter_public_key_edit_text_hint" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/large_spacing"
|
||||
android:layout_marginTop="@dimen/medium_spacing"
|
||||
android:layout_marginRight="@dimen/large_spacing"
|
||||
android:textSize="@dimen/very_small_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:alpha="0.6"
|
||||
android:textAlignment="center"
|
||||
android:text="@string/fragment_enter_public_key_explanation" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/optionalContentContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<org.thoughtcrime.securesms.components.LabeledSeparatorView
|
||||
android:id="@+id/separatorView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginLeft="@dimen/large_spacing"
|
||||
android:layout_marginTop="@dimen/large_spacing"
|
||||
android:layout_marginRight="@dimen/large_spacing" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/publicKeyTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/large_spacing"
|
||||
android:layout_marginTop="@dimen/large_spacing"
|
||||
android:layout_marginRight="@dimen/large_spacing"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:fontFamily="@font/space_mono_regular"
|
||||
android:textAlignment="center"
|
||||
tools:text="05987d601943c267879be41830888066c6a024cbdc9a548d06813924bf3372ea78" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/large_spacing"
|
||||
android:layout_marginTop="@dimen/large_spacing"
|
||||
android:layout_marginRight="@dimen/large_spacing"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.UnimportantFilled"
|
||||
android:id="@+id/copyButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/medium_button_height"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/copy" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.UnimportantFilled"
|
||||
android:id="@+id/shareButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/medium_button_height"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="@dimen/medium_spacing"
|
||||
android:text="@string/share" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/spacer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.ProminentOutline"
|
||||
android:id="@+id/createPrivateChatButton"
|
||||
android:layout_width="196dp"
|
||||
android:layout_height="@dimen/medium_button_height"
|
||||
android:layout_marginBottom="@dimen/medium_spacing"
|
||||
android:text="@string/next" />
|
||||
|
||||
</LinearLayout>
|
@ -1,86 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" >
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/mainContentContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<EditText
|
||||
style="@style/SmallSessionEditText"
|
||||
android:id="@+id/nameEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginLeft="@dimen/large_spacing"
|
||||
android:layout_marginTop="@dimen/medium_spacing"
|
||||
android:layout_marginRight="@dimen/large_spacing"
|
||||
android:layout_marginBottom="@dimen/medium_spacing"
|
||||
android:hint="@string/activity_create_closed_group_edit_text_hint" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:background="?android:dividerHorizontal" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/emptyStateContainer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical"
|
||||
android:layout_centerInParent="true">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:text="@string/activity_create_closed_group_empty_state_message" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.ProminentOutline"
|
||||
android:id="@+id/createNewPrivateChatButton"
|
||||
android:layout_width="196dp"
|
||||
android:layout_height="@dimen/medium_button_height"
|
||||
android:layout_marginTop="@dimen/medium_spacing"
|
||||
android:text="@string/activity_create_closed_group_empty_state_button_title" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/loaderContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#A4000000"
|
||||
android:visibility="gone"
|
||||
android:alpha="0">
|
||||
|
||||
<com.github.ybq.android.spinkit.SpinKitView
|
||||
style="@style/SpinKitView.Large.ThreeBounce"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_centerInParent="true"
|
||||
app:SpinKit_Color="@android:color/white" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</RelativeLayout>
|
@ -1,40 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/rootLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
android:id="@+id/viewPager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" >
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
style="@style/Widget.Session.TabLayout"
|
||||
android:id="@+id/tabLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/tab_bar_height" />
|
||||
|
||||
</androidx.viewpager.widget.ViewPager>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/loader"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#A4000000"
|
||||
android:visibility="gone"
|
||||
android:alpha="0">
|
||||
|
||||
<com.github.ybq.android.spinkit.SpinKitView
|
||||
style="@style/SpinKitView.Large.ThreeBounce"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_centerInParent="true"
|
||||
app:SpinKit_Color="@android:color/white" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</RelativeLayout>
|
@ -157,12 +157,16 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<org.thoughtcrime.securesms.home.NewConversationButtonSetView
|
||||
android:id="@+id/newConversationButtonSet"
|
||||
android:layout_width="276dp"
|
||||
android:layout_height="236dp"
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/newConversationButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_centerHorizontal="true" />
|
||||
android:layout_centerHorizontal="true"
|
||||
android:background="@drawable/new_conversation_button_background"
|
||||
android:layout_marginBottom="@dimen/new_conversation_button_bottom_offset"
|
||||
android:src="@drawable/ic_plus"
|
||||
app:tint="@color/white" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
23
app/src/main/res/layout/contact_section_header.xml
Normal file
23
app/src/main/res/layout/contact_section_header.xml
Normal file
@ -0,0 +1,23 @@
|
||||
<?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:background="@color/bottom_sheet_background"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingVertical="@dimen/very_small_spacing"
|
||||
android:paddingHorizontal="@dimen/large_spacing"
|
||||
android:textColor="@color/text"
|
||||
tools:text="A" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:background="?dividerHorizontal" />
|
||||
|
||||
</LinearLayout>
|
@ -7,11 +7,10 @@
|
||||
style="?attr/chipStyle"
|
||||
app:chipStartPadding="4dp"
|
||||
app:chipBackgroundColor="@color/open_group_chip_color"
|
||||
android:layout_columnWeight="1"
|
||||
tools:text="Main Group"
|
||||
android:ellipsize="end"
|
||||
tools:layout_width="wrap_content"
|
||||
app:chipMinTouchTargetSize="0dp"
|
||||
android:layout_margin="4dp"
|
||||
android:layout_width="0dp"
|
||||
android:layout_width="156dp"
|
||||
android:layout_height="wrap_content" />
|
163
app/src/main/res/layout/fragment_create_group.xml
Normal file
163
app/src/main/res/layout/fragment_create_group.xml
Normal file
@ -0,0 +1,163 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/rootLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/bottom_sheet_background">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/titleText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/setting_button_height"
|
||||
android:gravity="center_horizontal|center_vertical"
|
||||
android:text="@string/activity_create_group_title"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="@dimen/very_large_font_size"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/backButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/medium_spacing"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/new_conversation_dialog_back_button_content_description"
|
||||
android:focusable="true"
|
||||
android:src="@drawable/ic_arrow_left_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/titleText"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/titleText" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/closeButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/medium_spacing"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/new_conversation_dialog_close_button_content_description"
|
||||
android:focusable="true"
|
||||
android:src="@drawable/ic_baseline_close_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/titleText"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/titleText" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/nameEditText"
|
||||
style="@style/SmallSessionEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginLeft="@dimen/large_spacing"
|
||||
android:layout_marginTop="@dimen/medium_spacing"
|
||||
android:layout_marginRight="@dimen/large_spacing"
|
||||
android:layout_marginBottom="@dimen/medium_spacing"
|
||||
android:hint="@string/activity_create_closed_group_edit_text_hint"
|
||||
android:maxLength="30"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/titleText" />
|
||||
|
||||
<org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView
|
||||
android:id="@+id/contactSearch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="@dimen/medium_spacing"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/nameEditText"
|
||||
app:search_hint="@string/search_contacts_hint"
|
||||
app:show_always="true" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="@dimen/small_spacing"
|
||||
android:layout_marginBottom="@dimen/large_spacing"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/createClosedGroupButton"
|
||||
app:layout_constraintTop_toBottomOf="@id/contactSearch"
|
||||
tools:itemCount="5"
|
||||
tools:listitem="@layout/view_user" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/createClosedGroupButton"
|
||||
style="@style/Widget.Session.Button.Common.ProminentOutline"
|
||||
android:layout_width="196dp"
|
||||
android:layout_height="@dimen/medium_button_height"
|
||||
android:layout_marginVertical="@dimen/large_spacing"
|
||||
android:text="@string/activity_create_group_create_button_title"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/recyclerView"/>
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/mainContentGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:constraint_referenced_ids="nameEditText, contactSearch, recyclerView, createClosedGroupButton"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emptyStateMessageTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/activity_create_closed_group_empty_state_message"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
app:layout_constraintBottom_toTopOf="@id/createNewPrivateChatButton"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/titleText" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/createNewPrivateChatButton"
|
||||
style="@style/Widget.Session.Button.Common.ProminentOutline"
|
||||
android:layout_width="196dp"
|
||||
android:layout_height="@dimen/medium_button_height"
|
||||
android:layout_marginVertical="@dimen/medium_spacing"
|
||||
android:text="@string/activity_create_closed_group_empty_state_button_title"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/emptyStateMessageTextView" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/emptyStateGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:constraint_referenced_ids="emptyStateMessageTextView, createNewPrivateChatButton"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/loaderContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:alpha="0"
|
||||
android:background="#A4000000"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.github.ybq.android.spinkit.SpinKitView
|
||||
style="@style/SpinKitView.Large.ThreeBounce"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_marginTop="8dp"
|
||||
app:SpinKit_Color="@android:color/white" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -1,88 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/contentView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:background="?android:dividerHorizontal"
|
||||
android:elevation="1dp" />
|
||||
|
||||
<EditText
|
||||
style="@style/SmallSessionEditText"
|
||||
android:id="@+id/chatURLEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginLeft="@dimen/large_spacing"
|
||||
android:layout_marginTop="@dimen/large_spacing"
|
||||
android:layout_marginRight="@dimen/large_spacing"
|
||||
android:inputType="textWebEmailAddress|textMultiLine"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingBottom="0dp"
|
||||
android:gravity="center_vertical"
|
||||
android:maxLines="3"
|
||||
android:hint="@string/fragment_enter_chat_url_edit_text_hint" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/defaultRoomsLoaderContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:layout_marginTop="56dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<com.github.ybq.android.spinkit.SpinKitView
|
||||
android:id="@+id/defaultRoomsLoader"
|
||||
style="@style/SpinKitView.Large.ThreeBounce"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:SpinKit_Color="@color/text"
|
||||
android:visibility="gone"
|
||||
android:layout_centerInParent="true" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:visibility="gone"
|
||||
android:id="@+id/defaultRoomsContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_marginVertical="16dp"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:paddingHorizontal="24dp"
|
||||
android:text="@string/activity_join_public_chat_join_rooms"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<GridLayout
|
||||
android:id="@+id/defaultRoomsGridLayout"
|
||||
android:columnCount="2"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.ProminentOutline"
|
||||
android:id="@+id/joinPublicChatButton"
|
||||
android:layout_width="196dp"
|
||||
android:layout_height="@dimen/medium_button_height"
|
||||
android:layout_marginBottom="@dimen/medium_spacing"
|
||||
android:text="@string/next" />
|
||||
|
||||
</LinearLayout>
|
103
app/src/main/res/layout/fragment_enter_community_url.xml
Normal file
103
app/src/main/res/layout/fragment_enter_community_url.xml
Normal file
@ -0,0 +1,103 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/mainContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<View
|
||||
android:id="@+id/divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:background="?android:dividerHorizontal"
|
||||
android:elevation="1dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/communityUrlEditText"
|
||||
style="@style/SmallSessionEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginHorizontal="@dimen/large_spacing"
|
||||
android:layout_marginTop="@dimen/large_spacing"
|
||||
android:gravity="center_vertical"
|
||||
android:hint="@string/fragment_enter_community_url_edit_text_hint"
|
||||
android:inputType="textWebEmailAddress|textMultiLine"
|
||||
android:maxLines="3"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingBottom="0dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/divider" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/defaultRoomsLoaderContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:layout_marginTop="56dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/communityUrlEditText">
|
||||
|
||||
<com.github.ybq.android.spinkit.SpinKitView
|
||||
android:id="@+id/defaultRoomsLoader"
|
||||
style="@style/SpinKitView.Large.ThreeBounce"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:visibility="gone"
|
||||
app:SpinKit_Color="@color/text" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/defaultRoomsContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@id/joinCommunityButton"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/communityUrlEditText"
|
||||
tools:visibility="visible">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="16dp"
|
||||
android:paddingHorizontal="24dp"
|
||||
android:text="@string/activity_join_public_chat_join_rooms"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.google.android.flexbox.FlexboxLayout
|
||||
android:id="@+id/defaultRoomsFlexboxLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:paddingHorizontal="16dp"
|
||||
app:alignItems="center"
|
||||
app:flexDirection="row"
|
||||
app:flexWrap="wrap"
|
||||
app:justifyContent="center" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/joinCommunityButton"
|
||||
style="@style/Widget.Session.Button.Common.ProminentOutline"
|
||||
android:layout_width="196dp"
|
||||
android:layout_height="@dimen/medium_button_height"
|
||||
android:layout_marginVertical="@dimen/large_spacing"
|
||||
android:text="@string/fragment_enter_community_url_join_button_title"
|
||||
android:enabled="false"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -1,52 +1,72 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<org.thoughtcrime.securesms.components.NestedScrollableHost xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/contentView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
android:fillViewport="true">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/mainContainer">
|
||||
|
||||
<View
|
||||
android:id="@+id/divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:background="?android:dividerHorizontal"
|
||||
android:elevation="1dp" />
|
||||
android:elevation="1dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<EditText
|
||||
style="@style/SmallSessionEditText"
|
||||
android:id="@+id/publicKeyEditText"
|
||||
style="@style/SmallSessionEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginLeft="@dimen/large_spacing"
|
||||
android:layout_marginHorizontal="@dimen/large_spacing"
|
||||
android:layout_marginTop="@dimen/large_spacing"
|
||||
android:layout_marginRight="@dimen/large_spacing"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingBottom="0dp"
|
||||
android:gravity="center_vertical"
|
||||
android:hint="@string/fragment_enter_public_key_edit_text_hint"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textMultiLine"
|
||||
android:maxLines="3"
|
||||
android:imeOptions="actionDone"
|
||||
android:hint="@string/fragment_enter_public_key_edit_text_hint" />
|
||||
android:paddingTop="0dp"
|
||||
android:paddingBottom="0dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/divider" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/promptTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/large_spacing"
|
||||
android:layout_marginHorizontal="@dimen/large_spacing"
|
||||
android:layout_marginTop="@dimen/medium_spacing"
|
||||
android:layout_marginRight="@dimen/large_spacing"
|
||||
android:textSize="@dimen/very_small_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:alpha="0.6"
|
||||
android:text="@string/fragment_enter_public_key_prompt"
|
||||
android:textAlignment="center"
|
||||
android:text="@string/fragment_enter_public_key_explanation" />
|
||||
android:textColor="@color/text"
|
||||
android:textSize="@dimen/very_small_font_size"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/publicKeyEditText" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/optionalContentContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toTopOf="@id/createPrivateChatButton"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/promptTextView">
|
||||
|
||||
<org.thoughtcrime.securesms.components.LabeledSeparatorView
|
||||
android:id="@+id/separatorView"
|
||||
@ -56,60 +76,69 @@
|
||||
android:layout_marginTop="@dimen/large_spacing"
|
||||
android:layout_marginRight="@dimen/large_spacing" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/qrCodeImageView"
|
||||
android:layout_width="228dp"
|
||||
android:layout_height="228dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="@dimen/medium_spacing"
|
||||
tools:src="@drawable/ic_qr_code_24" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/publicKeyTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/large_spacing"
|
||||
android:layout_marginHorizontal="@dimen/large_spacing"
|
||||
android:layout_marginTop="@dimen/large_spacing"
|
||||
android:layout_marginRight="@dimen/large_spacing"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
android:textColor="@color/text"
|
||||
android:fontFamily="@font/space_mono_regular"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
tools:text="05987d601943c267879be41830888066c6a024cbdc9a548d06813924bf3372ea78" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="@dimen/large_spacing"
|
||||
android:layout_marginTop="@dimen/large_spacing"
|
||||
android:layout_marginRight="@dimen/large_spacing"
|
||||
android:layout_marginHorizontal="@dimen/large_spacing"
|
||||
android:layout_marginVertical="@dimen/large_spacing"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.UnimportantFilled"
|
||||
android:id="@+id/copyButton"
|
||||
style="@style/Widget.Session.Button.Common.ProminentOutline"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/medium_button_height"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/copy" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.UnimportantFilled"
|
||||
android:id="@+id/shareButton"
|
||||
style="@style/Widget.Session.Button.Common.ProminentOutline"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/medium_button_height"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="@dimen/medium_spacing"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/share" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/spacer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<Button
|
||||
style="@style/Widget.Session.Button.Common.ProminentOutline"
|
||||
android:id="@+id/createPrivateChatButton"
|
||||
style="@style/Widget.Session.Button.Common.ProminentOutline"
|
||||
android:layout_width="196dp"
|
||||
android:layout_height="@dimen/medium_button_height"
|
||||
android:layout_marginBottom="@dimen/medium_spacing"
|
||||
android:text="@string/next" />
|
||||
android:layout_marginVertical="@dimen/medium_spacing"
|
||||
android:text="@string/next"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</org.thoughtcrime.securesms.components.NestedScrollableHost>
|
87
app/src/main/res/layout/fragment_join_community.xml
Normal file
87
app/src/main/res/layout/fragment_join_community.xml
Normal file
@ -0,0 +1,87 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/bottom_sheet_background">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/titleText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/setting_button_height"
|
||||
android:gravity="center_horizontal|center_vertical"
|
||||
android:text="@string/dialog_join_community_title"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="@dimen/very_large_font_size"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/backButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/medium_spacing"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:src="@drawable/ic_arrow_left_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/titleText"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/titleText"
|
||||
android:contentDescription="@string/new_conversation_dialog_back_button_content_description" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/closeButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/medium_spacing"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:src="@drawable/ic_baseline_close_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/titleText"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/titleText"
|
||||
android:contentDescription="@string/new_conversation_dialog_close_button_content_description" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabLayout"
|
||||
style="@style/Widget.Session.TabLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/tab_bar_height"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/titleText" />
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/viewPager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tabLayout" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/loader"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:alpha="0"
|
||||
android:background="#A4000000"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<com.github.ybq.android.spinkit.SpinKitView
|
||||
style="@style/SpinKitView.Large.ThreeBounce"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_marginTop="8dp"
|
||||
app:SpinKit_Color="@android:color/white" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
5
app/src/main/res/layout/fragment_new_conversation.xml
Normal file
5
app/src/main/res/layout/fragment_new_conversation.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/new_conversation_fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
158
app/src/main/res/layout/fragment_new_conversation_home.xml
Normal file
158
app/src/main/res/layout/fragment_new_conversation_home.xml
Normal file
@ -0,0 +1,158 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/bottom_sheet_background">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/constraintLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="@dimen/medium_spacing">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/titleText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_horizontal|center_vertical"
|
||||
android:paddingTop="@dimen/large_spacing"
|
||||
android:paddingBottom="@dimen/very_large_spacing"
|
||||
android:text="@string/dialog_new_conversation_title"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="@dimen/very_large_font_size"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/closeButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/large_spacing"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/new_conversation_dialog_close_button_content_description"
|
||||
android:focusable="true"
|
||||
android:src="@drawable/ic_baseline_close_24"
|
||||
app:layout_constraintBottom_toTopOf="@id/title_divider"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<View
|
||||
android:id="@+id/title_divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:layout_marginBottom="1dp"
|
||||
android:background="?android:dividerHorizontal"
|
||||
app:layout_constraintStart_toStartOf="@+id/titleText"
|
||||
app:layout_constraintTop_toBottomOf="@+id/titleText" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/createPrivateChatButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/setting_button_height"
|
||||
android:background="@color/bottom_sheet_item_background"
|
||||
android:drawablePadding="@dimen/large_spacing"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingHorizontal="@dimen/large_spacing"
|
||||
android:text="@string/dialog_new_message_title"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
app:drawableStartCompat="@drawable/ic_message"
|
||||
app:layout_constraintBottom_toTopOf="@+id/new_message_divider"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/title_divider" />
|
||||
|
||||
<View
|
||||
android:id="@+id/new_message_divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:layout_marginStart="@dimen/massive_spacing"
|
||||
android:background="?android:dividerHorizontal"
|
||||
app:layout_constraintStart_toStartOf="@+id/createPrivateChatButton"
|
||||
app:layout_constraintTop_toBottomOf="@+id/createPrivateChatButton" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/createClosedGroupButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/setting_button_height"
|
||||
android:background="@color/bottom_sheet_item_background"
|
||||
android:drawablePadding="@dimen/large_spacing"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingHorizontal="@dimen/large_spacing"
|
||||
android:text="@string/activity_create_group_title"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
app:drawableStartCompat="@drawable/ic_group"
|
||||
app:layout_constraintBottom_toTopOf="@+id/create_group_divider"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/new_message_divider" />
|
||||
|
||||
<View
|
||||
android:id="@+id/create_group_divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:layout_marginStart="@dimen/massive_spacing"
|
||||
android:background="?android:dividerHorizontal"
|
||||
app:layout_constraintStart_toStartOf="@+id/createClosedGroupButton"
|
||||
app:layout_constraintTop_toBottomOf="@+id/createClosedGroupButton" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/joinCommunityButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/setting_button_height"
|
||||
android:background="@color/bottom_sheet_item_background"
|
||||
android:drawablePadding="@dimen/large_spacing"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingHorizontal="@dimen/large_spacing"
|
||||
android:text="@string/dialog_join_community_title"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
app:drawableStartCompat="@drawable/ic_globe"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/create_group_divider" />
|
||||
|
||||
<View
|
||||
android:id="@+id/join_community_divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:layout_marginBottom="1dp"
|
||||
android:background="?android:dividerHorizontal"
|
||||
app:layout_constraintStart_toStartOf="@+id/joinCommunityButton"
|
||||
app:layout_constraintTop_toBottomOf="@+id/joinCommunityButton" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/contactsText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingHorizontal="@dimen/large_spacing"
|
||||
android:paddingTop="@dimen/medium_spacing"
|
||||
android:paddingBottom="@dimen/small_spacing"
|
||||
android:text="@string/new_conversation_contacts_title"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/join_community_divider" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/contactsRecyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/contactsText"
|
||||
tools:itemCount="6"
|
||||
tools:listitem="@layout/view_contact" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
87
app/src/main/res/layout/fragment_new_message.xml
Normal file
87
app/src/main/res/layout/fragment_new_message.xml
Normal file
@ -0,0 +1,87 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/rootLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/bottom_sheet_background">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/titleText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/setting_button_height"
|
||||
android:gravity="center_horizontal|center_vertical"
|
||||
android:text="@string/dialog_new_message_title"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="@dimen/very_large_font_size"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/backButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/medium_spacing"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:src="@drawable/ic_arrow_left_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/titleText"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/titleText"
|
||||
android:contentDescription="@string/new_conversation_dialog_back_button_content_description" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/closeButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/medium_spacing"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:src="@drawable/ic_baseline_close_24"
|
||||
app:layout_constraintBottom_toBottomOf="@id/titleText"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/titleText"
|
||||
android:contentDescription="@string/new_conversation_dialog_close_button_content_description" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabLayout"
|
||||
style="@style/Widget.Session.TabLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/tab_bar_height"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/titleText" />
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/viewPager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tabLayout" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/loader"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:alpha="0"
|
||||
android:background="#A4000000"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/viewPager">
|
||||
|
||||
<com.github.ybq.android.spinkit.SpinKitView
|
||||
style="@style/SpinKitView.Large.ThreeBounce"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_marginTop="8dp"
|
||||
app:SpinKit_Color="@android:color/white" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<View xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="0dp"
|
||||
android:layout_columnWeight="1"
|
||||
android:layout_height="0dp"/>
|
43
app/src/main/res/layout/view_contact.xml
Normal file
43
app/src/main/res/layout/view_contact.xml
Normal file
@ -0,0 +1,43 @@
|
||||
<?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:background="@color/bottom_sheet_item_background"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/contentView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingHorizontal="@dimen/large_spacing"
|
||||
android:paddingVertical="@dimen/small_spacing">
|
||||
|
||||
<include layout="@layout/view_profile_picture"
|
||||
android:id="@+id/profilePictureView"
|
||||
android:layout_width="@dimen/small_profile_picture_size"
|
||||
android:layout_height="@dimen/small_profile_picture_size" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nameTextView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/medium_spacing"
|
||||
android:maxLines="1"
|
||||
android:textAlignment="viewStart"
|
||||
android:ellipsize="end"
|
||||
android:textSize="@dimen/medium_font_size"
|
||||
android:textStyle="bold"
|
||||
android:layout_weight="1"
|
||||
tools:text="Spiderman" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1px"
|
||||
android:background="?android:dividerHorizontal" />
|
||||
|
||||
</LinearLayout>
|
@ -34,6 +34,9 @@
|
||||
<color name="conversation_pinned_background">#F0F0F0</color>
|
||||
<color name="conversation_pinned_icon">#606060</color>
|
||||
|
||||
<color name="bottom_sheet_background">#F9F9F9</color>
|
||||
<color name="bottom_sheet_item_background">@color/white</color>
|
||||
|
||||
<color name="default_background_start">#ffffff</color>
|
||||
<color name="default_background_end">#fcfcfc</color>
|
||||
<color name="action_bar_background">#fcfcfc</color>
|
||||
|
@ -46,6 +46,9 @@
|
||||
<color name="call_action_foreground_highlighted">#171717</color>
|
||||
<color name="call_background">#171717</color>
|
||||
|
||||
<color name="bottom_sheet_background">#1B1B1B</color>
|
||||
<color name="bottom_sheet_item_background">#1B1B1B</color>
|
||||
|
||||
<array name="profile_picture_placeholder_colors">
|
||||
<item>#5ff8b0</item>
|
||||
<item>#26cdb9</item>
|
||||
|
@ -961,4 +961,19 @@
|
||||
<item quantity="one">And %1$d other has reacted %2$s to this message</item>
|
||||
<item quantity="other">And %1$d others have reacted %2$s to this message</item>
|
||||
</plurals>
|
||||
|
||||
<string name="dialog_new_conversation_title">New Conversation</string>
|
||||
<string name="dialog_new_message_title">New Message</string>
|
||||
<string name="activity_create_group_title">Create Group</string>
|
||||
<string name="dialog_join_community_title">Join Community</string>
|
||||
<string name="new_conversation_contacts_title">Contacts</string>
|
||||
<string name="new_conversation_unknown_contacts_section_title">Unknown</string>
|
||||
<string name="fragment_enter_public_key_prompt">Start a new conversation by entering someone\'s Session ID or share your Session ID with them.</string>
|
||||
<string name="activity_create_group_create_button_title">Create</string>
|
||||
<string name="search_contacts_hint">Search contacts</string>
|
||||
<string name="activity_join_public_chat_enter_community_url_tab_title">Community URL</string>
|
||||
<string name="fragment_enter_community_url_edit_text_hint">Enter Community URL</string>
|
||||
<string name="fragment_enter_community_url_join_button_title">Join</string>
|
||||
<string name="new_conversation_dialog_back_button_content_description">Navigate Back</string>
|
||||
<string name="new_conversation_dialog_close_button_content_description">Close Dialog</string>
|
||||
</resources>
|
||||
|
@ -50,7 +50,6 @@
|
||||
<style name="Widget.Session.TabLayout" parent="Widget.Design.TabLayout">
|
||||
<item name="elevation">1dp</item>
|
||||
<item name="tabIndicatorColor">?colorAccent</item>
|
||||
<item name="tabSelectedTextColor">?colorAccent</item>>
|
||||
<item name="tabIndicatorHeight">@dimen/accent_line_thickness</item>
|
||||
<item name="tabRippleColor">@color/cell_selected</item>
|
||||
<item name="tabTextAppearance">@style/TextAppearance.Session.Tab</item>
|
||||
|
Loading…
Reference in New Issue
Block a user