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>
pull/991/head
ceokot 2 years ago committed by GitHub
parent 3fcd972c2a
commit fbd1721eaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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
@ -76,10 +74,9 @@ import javax.inject.Inject
@AndroidEntryPoint
class HomeActivity : PassphraseRequiredActionBarActivity(),
ConversationClickListener,
SeedReminderViewDelegate,
NewConversationButtonSetViewDelegate,
GlobalSearchInputLayout.GlobalSearchInputLayoutListener {
ConversationClickListener,
SeedReminderViewDelegate,
GlobalSearchInputLayout.GlobalSearchInputLayoutListener {
private lateinit var binding: ActivityHomeBinding
private lateinit var glide: GlideRequests
@ -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)
}
override fun createNewClosedGroup() {
val intent = Intent(this, CreateClosedGroupActivity::class.java)
show(intent, true)
private fun showNewConversation() {
NewConversationFragment().show(supportFragmentManager, "NewConversationFragment")
}
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)
}

@ -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>

@ -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" />

@ -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>

@ -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,115 +1,144 @@
<?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"
android:gravity="center_horizontal"
android:orientation="vertical">
android:layout_height="match_parent">
<View
<ScrollView
android:layout_width="match_parent"
android:layout_height="1px"
android:background="?android:dividerHorizontal"
android:elevation="1dp" />
android:layout_height="match_parent"
android:fillViewport="true">
<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"
<androidx.constraintlayout.widget.ConstraintLayout
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">
android:id="@+id/mainContainer">
<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" />
<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" />
<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" />
<EditText
android:id="@+id/publicKeyEditText"
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_public_key_edit_text_hint"
android:imeOptions="actionDone"
android:inputType="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" />
</LinearLayout>
<TextView
android:id="@+id/promptTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/large_spacing"
android:layout_marginTop="@dimen/medium_spacing"
android:alpha="0.6"
android:text="@string/fragment_enter_public_key_prompt"
android:textAlignment="center"
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>
<LinearLayout
android:id="@+id/optionalContentContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
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"
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" />
<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_marginHorizontal="@dimen/large_spacing"
android:layout_marginTop="@dimen/large_spacing"
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_marginHorizontal="@dimen/large_spacing"
android:layout_marginVertical="@dimen/large_spacing"
android:orientation="horizontal">
<Button
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
android:id="@+id/shareButton"
style="@style/Widget.Session.Button.Common.ProminentOutline"
android:layout_width="0dp"
android:layout_height="@dimen/medium_button_height"
android:layout_marginStart="@dimen/medium_spacing"
android:layout_weight="1"
android:text="@string/share" />
</LinearLayout>
</LinearLayout>
<Button
android:id="@+id/createPrivateChatButton"
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/next"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible" />
<View
android:id="@+id/spacer"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
</androidx.constraintlayout.widget.ConstraintLayout>
<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" />
</ScrollView>
</LinearLayout>
</org.thoughtcrime.securesms.components.NestedScrollableHost>

@ -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>

@ -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" />

@ -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>

@ -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"/>

@ -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…
Cancel
Save