Merge pull request #619 from vector-im/feature/attachments
Feature/attachments
This commit is contained in:
commit
70a14f6350
|
@ -14,3 +14,5 @@
|
|||
/tmp
|
||||
|
||||
ktlint
|
||||
.idea/copyright/New_vector.xml
|
||||
.idea/copyright/profiles_settings.xml
|
||||
|
|
|
@ -8,6 +8,8 @@ Improvements:
|
|||
- Persist active tab between sessions (#503)
|
||||
- Do not upload file too big for the homeserver (#587)
|
||||
- Handle read markers (#84)
|
||||
- Attachments: start using system pickers (#52)
|
||||
- Attachments: start handling incoming share (#58)
|
||||
- Mark all messages as read (#396)
|
||||
- Add ability to report content (#515)
|
||||
|
||||
|
|
|
@ -316,7 +316,7 @@ dependencies {
|
|||
implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
|
||||
|
||||
// File picker
|
||||
implementation 'com.github.jaiselrahman:FilePicker:1.2.2'
|
||||
implementation 'com.kbeanie:multipicker:1.6@aar'
|
||||
|
||||
// DI
|
||||
implementation "com.google.dagger:dagger:$daggerVersion"
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
package="im.vector.riotx">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
|
||||
<application
|
||||
android:name=".VectorApplication"
|
||||
|
@ -79,6 +80,22 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".features.share.IncomingShareActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<data android:mimeType="*/*" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.OPENABLE" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<data android:mimeType="*/*" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.OPENABLE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Services -->
|
||||
|
||||
<service
|
||||
|
|
|
@ -339,11 +339,6 @@ SOFTWARE.
|
|||
<br/>
|
||||
Copyright 2014 Leo Lin
|
||||
</li>
|
||||
<li>
|
||||
<b>FilePicker</b>
|
||||
<br/>
|
||||
Copyright (c) 2018, Jaisel Rahman
|
||||
</li>
|
||||
<li>
|
||||
<b>diff-match-patch</b>
|
||||
<br/>
|
||||
|
@ -359,6 +354,11 @@ SOFTWARE.
|
|||
<br/>
|
||||
Copyright 2017 Gabriel Ittner.
|
||||
</li>
|
||||
<li>
|
||||
<b>Android-multipicker-library</b>
|
||||
<br/>
|
||||
Copyright 2018 Kumar Bibek
|
||||
</li>
|
||||
</ul>
|
||||
<pre>
|
||||
Apache License
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* 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 im.vector.riotx
|
||||
|
||||
import arrow.core.Option
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.riotx.core.utils.RxStore
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ActiveSessionObservableStore @Inject constructor() : RxStore<Option<Session>>()
|
|
@ -14,56 +14,59 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.home
|
||||
package im.vector.riotx
|
||||
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleObserver
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import arrow.core.Option
|
||||
import com.airbnb.mvrx.ActivityViewModelContext
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.group.model.GroupSummary
|
||||
import im.vector.matrix.android.api.session.room.model.RoomSummary
|
||||
import im.vector.matrix.rx.rx
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import im.vector.riotx.features.home.HomeRoomListObservableStore
|
||||
import im.vector.riotx.features.home.group.ALL_COMMUNITIES_GROUP_ID
|
||||
import im.vector.riotx.features.home.group.SelectedGroupStore
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.functions.BiFunction
|
||||
import io.reactivex.rxkotlin.addTo
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
data class EmptyState(val isEmpty: Boolean = true) : MvRxState
|
||||
/**
|
||||
* This class handles the global app state. At the moment, it only manages room list.
|
||||
* It requires to be added to ProcessLifecycleOwner.get().lifecycle
|
||||
*/
|
||||
@Singleton
|
||||
class AppStateHandler @Inject constructor(
|
||||
private val sessionObservableStore: ActiveSessionObservableStore,
|
||||
private val homeRoomListStore: HomeRoomListObservableStore,
|
||||
private val selectedGroupStore: SelectedGroupStore) : LifecycleObserver {
|
||||
|
||||
class HomeActivityViewModel @AssistedInject constructor(@Assisted initialState: EmptyState,
|
||||
private val session: Session,
|
||||
private val selectedGroupStore: SelectedGroupStore,
|
||||
private val homeRoomListStore: HomeRoomListObservableStore
|
||||
) : VectorViewModel<EmptyState>(initialState) {
|
||||
private val compositeDisposable = CompositeDisposable()
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: EmptyState): HomeActivityViewModel
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
fun entersForeground() {
|
||||
observeRoomsAndGroup()
|
||||
}
|
||||
|
||||
companion object : MvRxViewModelFactory<HomeActivityViewModel, EmptyState> {
|
||||
|
||||
@JvmStatic
|
||||
override fun create(viewModelContext: ViewModelContext, state: EmptyState): HomeActivityViewModel? {
|
||||
val homeActivity: HomeActivity = (viewModelContext as ActivityViewModelContext).activity()
|
||||
return homeActivity.homeActivityViewModelFactory.create(state)
|
||||
}
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
fun entersBackground() {
|
||||
compositeDisposable.clear()
|
||||
}
|
||||
|
||||
init {
|
||||
observeRoomAndGroup()
|
||||
}
|
||||
|
||||
private fun observeRoomAndGroup() {
|
||||
private fun observeRoomsAndGroup() {
|
||||
Observable
|
||||
.combineLatest<List<RoomSummary>, Option<GroupSummary>, List<RoomSummary>>(
|
||||
session.rx().liveRoomSummaries().throttleLast(300, TimeUnit.MILLISECONDS),
|
||||
sessionObservableStore.observe()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.switchMap {
|
||||
it.orNull()?.rx()?.liveRoomSummaries()
|
||||
?: Observable.just(emptyList())
|
||||
}
|
||||
.throttleLast(300, TimeUnit.MILLISECONDS),
|
||||
selectedGroupStore.observe(),
|
||||
BiFunction { rooms, selectedGroupOption ->
|
||||
val selectedGroup = selectedGroupOption.orNull()
|
||||
|
@ -91,6 +94,6 @@ class HomeActivityViewModel @AssistedInject constructor(@Assisted initialState:
|
|||
.subscribe {
|
||||
homeRoomListStore.post(it)
|
||||
}
|
||||
.disposeOnClear()
|
||||
.addTo(compositeDisposable)
|
||||
}
|
||||
}
|
|
@ -74,6 +74,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
|
|||
@Inject lateinit var vectorPreferences: VectorPreferences
|
||||
@Inject lateinit var versionProvider: VersionProvider
|
||||
@Inject lateinit var notificationUtils: NotificationUtils
|
||||
@Inject lateinit var appStateHandler: AppStateHandler
|
||||
lateinit var vectorComponent: VectorComponent
|
||||
private var fontThreadHandler: Handler? = null
|
||||
|
||||
|
@ -134,6 +135,7 @@ class VectorApplication : Application(), HasVectorInjector, MatrixConfiguration.
|
|||
FcmHelper.onEnterBackground(appContext, vectorPreferences, activeSessionHolder)
|
||||
}
|
||||
})
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(appStateHandler)
|
||||
// This should be done as early as possible
|
||||
initKnownEmojiHashSet(appContext)
|
||||
}
|
||||
|
|
|
@ -16,8 +16,10 @@
|
|||
|
||||
package im.vector.riotx.core.di
|
||||
|
||||
import arrow.core.Option
|
||||
import im.vector.matrix.android.api.auth.Authenticator
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.riotx.ActiveSessionObservableStore
|
||||
import im.vector.riotx.features.crypto.keysrequest.KeyRequestHandler
|
||||
import im.vector.riotx.features.crypto.verification.IncomingVerificationRequestHandler
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
@ -26,6 +28,7 @@ import javax.inject.Singleton
|
|||
|
||||
@Singleton
|
||||
class ActiveSessionHolder @Inject constructor(private val authenticator: Authenticator,
|
||||
private val sessionObservableStore: ActiveSessionObservableStore,
|
||||
private val keyRequestHandler: KeyRequestHandler,
|
||||
private val incomingVerificationRequestHandler: IncomingVerificationRequestHandler
|
||||
) {
|
||||
|
@ -34,12 +37,14 @@ class ActiveSessionHolder @Inject constructor(private val authenticator: Authent
|
|||
|
||||
fun setActiveSession(session: Session) {
|
||||
activeSession.set(session)
|
||||
sessionObservableStore.post(Option.fromNullable(session))
|
||||
keyRequestHandler.start(session)
|
||||
incomingVerificationRequestHandler.start(session)
|
||||
}
|
||||
|
||||
fun clearActiveSession() {
|
||||
activeSession.set(null)
|
||||
sessionObservableStore.post(Option.empty())
|
||||
keyRequestHandler.stop()
|
||||
incomingVerificationRequestHandler.stop()
|
||||
}
|
||||
|
|
|
@ -67,6 +67,7 @@ import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment
|
|||
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment
|
||||
import im.vector.riotx.features.settings.*
|
||||
import im.vector.riotx.features.settings.push.PushGatewaysFragment
|
||||
import im.vector.riotx.features.share.IncomingShareActivity
|
||||
import im.vector.riotx.features.ui.UiStateRepository
|
||||
|
||||
@Component(dependencies = [VectorComponent::class], modules = [AssistedInjectModule::class, ViewModelModule::class, HomeModule::class])
|
||||
|
@ -183,6 +184,8 @@ interface ScreenComponent {
|
|||
|
||||
fun inject(reactionButton: ReactionButton)
|
||||
|
||||
fun inject(incomingShareActivity: IncomingShareActivity)
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
fun create(vectorComponent: VectorComponent,
|
||||
|
|
|
@ -48,3 +48,10 @@ fun EditText.showPassword(visible: Boolean, updateCursor: Boolean = true) {
|
|||
}
|
||||
if (updateCursor) setSelection(text?.length ?: 0)
|
||||
}
|
||||
|
||||
fun View.getMeasurements(): Pair<Int, Int> {
|
||||
measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
|
||||
val width = measuredWidth
|
||||
val height = measuredHeight
|
||||
return width to height
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* 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 im.vector.riotx.core.utils
|
||||
|
||||
import android.app.Activity
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
|
||||
class KeyboardStateUtils(activity: Activity) : ViewTreeObserver.OnGlobalLayoutListener {
|
||||
|
||||
private val contentView: View = activity.findViewById<View>(android.R.id.content).also {
|
||||
it.viewTreeObserver.addOnGlobalLayoutListener(this)
|
||||
}
|
||||
var isKeyboardShowing: Boolean = false
|
||||
private set
|
||||
|
||||
override fun onGlobalLayout() {
|
||||
val rect = Rect()
|
||||
contentView.getWindowVisibleDisplayFrame(rect)
|
||||
val screenHeight = contentView.rootView.height
|
||||
|
||||
val keypadHeight = screenHeight - rect.bottom
|
||||
isKeyboardShowing = keypadHeight > screenHeight * 0.15
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@ import android.content.Context
|
|||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
|
@ -51,8 +52,9 @@ const val PERMISSIONS_FOR_MEMBER_DETAILS = PERMISSION_READ_CONTACTS
|
|||
const val PERMISSIONS_FOR_ROOM_AVATAR = PERMISSION_CAMERA
|
||||
const val PERMISSIONS_FOR_VIDEO_RECORDING = PERMISSION_CAMERA or PERMISSION_RECORD_AUDIO
|
||||
const val PERMISSIONS_FOR_WRITING_FILES = PERMISSION_WRITE_EXTERNAL_STORAGE
|
||||
const val PERMISSIONS_FOR_PICKING_CONTACT = PERMISSION_READ_CONTACTS
|
||||
|
||||
private const val PERMISSIONS_EMPTY = PERMISSION_BYPASSED
|
||||
const val PERMISSIONS_EMPTY = PERMISSION_BYPASSED
|
||||
|
||||
// Request code to ask permission to the system (arbitrary values)
|
||||
const val PERMISSION_REQUEST_CODE = 567
|
||||
|
@ -64,6 +66,7 @@ const val PERMISSION_REQUEST_CODE_VIDEO_CALL = 572
|
|||
const val PERMISSION_REQUEST_CODE_EXPORT_KEYS = 573
|
||||
const val PERMISSION_REQUEST_CODE_CHANGE_AVATAR = 574
|
||||
const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575
|
||||
const val PERMISSION_REQUEST_CODE_PICK_ATTACHMENT = 576
|
||||
|
||||
/**
|
||||
* Log the used permissions statuses.
|
||||
|
@ -98,8 +101,9 @@ fun logPermissionStatuses(context: Context) {
|
|||
*/
|
||||
fun checkPermissions(permissionsToBeGrantedBitMap: Int,
|
||||
activity: Activity,
|
||||
requestCode: Int = PERMISSION_REQUEST_CODE): Boolean {
|
||||
return checkPermissions(permissionsToBeGrantedBitMap, activity, null, requestCode)
|
||||
requestCode: Int,
|
||||
@StringRes rationaleMessage: Int = 0): Boolean {
|
||||
return checkPermissions(permissionsToBeGrantedBitMap, activity, null, requestCode, rationaleMessage)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -111,8 +115,9 @@ fun checkPermissions(permissionsToBeGrantedBitMap: Int,
|
|||
*/
|
||||
fun checkPermissions(permissionsToBeGrantedBitMap: Int,
|
||||
fragment: Fragment,
|
||||
requestCode: Int = PERMISSION_REQUEST_CODE): Boolean {
|
||||
return checkPermissions(permissionsToBeGrantedBitMap, fragment.activity, fragment, requestCode)
|
||||
requestCode: Int,
|
||||
@StringRes rationaleMessage: Int = 0): Boolean {
|
||||
return checkPermissions(permissionsToBeGrantedBitMap, fragment.activity, fragment, requestCode, rationaleMessage)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -136,7 +141,9 @@ fun checkPermissions(permissionsToBeGrantedBitMap: Int,
|
|||
private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
|
||||
activity: Activity?,
|
||||
fragment: Fragment?,
|
||||
requestCode: Int): Boolean {
|
||||
requestCode: Int,
|
||||
@StringRes rationaleMessage: Int
|
||||
): Boolean {
|
||||
var isPermissionGranted = false
|
||||
|
||||
// sanity check
|
||||
|
@ -159,7 +166,6 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
|
|||
val permissionListAlreadyDenied = ArrayList<String>()
|
||||
val permissionsListToBeGranted = ArrayList<String>()
|
||||
var isRequestPermissionRequired = false
|
||||
var explanationMessage = ""
|
||||
|
||||
// retrieve the permissions to be granted according to the request code bit map
|
||||
if (PERMISSION_CAMERA == permissionsToBeGrantedBitMap and PERMISSION_CAMERA) {
|
||||
|
@ -199,58 +205,11 @@ private fun checkPermissions(permissionsToBeGrantedBitMap: Int,
|
|||
}
|
||||
|
||||
// if some permissions were already denied: display a dialog to the user before asking again.
|
||||
if (!permissionListAlreadyDenied.isEmpty()) {
|
||||
if (permissionsToBeGrantedBitMap == PERMISSIONS_FOR_VIDEO_IP_CALL || permissionsToBeGrantedBitMap == PERMISSIONS_FOR_AUDIO_IP_CALL) {
|
||||
// Permission request for VOIP call
|
||||
if (permissionListAlreadyDenied.contains(Manifest.permission.CAMERA)
|
||||
&& permissionListAlreadyDenied.contains(Manifest.permission.RECORD_AUDIO)) {
|
||||
// Both missing
|
||||
explanationMessage += activity.getString(R.string.permissions_rationale_msg_camera_and_audio)
|
||||
} else if (permissionListAlreadyDenied.contains(Manifest.permission.RECORD_AUDIO)) {
|
||||
// Audio missing
|
||||
explanationMessage += activity.getString(R.string.permissions_rationale_msg_record_audio)
|
||||
explanationMessage += activity.getString(R.string.permissions_rationale_msg_record_audio_explanation)
|
||||
} else if (permissionListAlreadyDenied.contains(Manifest.permission.CAMERA)) {
|
||||
// Camera missing
|
||||
explanationMessage += activity.getString(R.string.permissions_rationale_msg_camera)
|
||||
explanationMessage += activity.getString(R.string.permissions_rationale_msg_camera_explanation)
|
||||
}
|
||||
} else {
|
||||
permissionListAlreadyDenied.forEach {
|
||||
when (it) {
|
||||
Manifest.permission.CAMERA -> {
|
||||
if (explanationMessage.isNotEmpty()) {
|
||||
explanationMessage += "\n\n"
|
||||
}
|
||||
explanationMessage += activity.getString(R.string.permissions_rationale_msg_camera)
|
||||
}
|
||||
Manifest.permission.RECORD_AUDIO -> {
|
||||
if (explanationMessage.isNotEmpty()) {
|
||||
explanationMessage += "\n\n"
|
||||
}
|
||||
explanationMessage += activity.getString(R.string.permissions_rationale_msg_record_audio)
|
||||
}
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE -> {
|
||||
if (explanationMessage.isNotEmpty()) {
|
||||
explanationMessage += "\n\n"
|
||||
}
|
||||
explanationMessage += activity.getString(R.string.permissions_rationale_msg_storage)
|
||||
}
|
||||
Manifest.permission.READ_CONTACTS -> {
|
||||
if (!explanationMessage.isEmpty()) {
|
||||
explanationMessage += "\n\n"
|
||||
}
|
||||
explanationMessage += activity.getString(R.string.permissions_rationale_msg_contacts)
|
||||
}
|
||||
else -> Timber.v("## checkPermissions(): already denied permission not supported")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (permissionListAlreadyDenied.isNotEmpty() && rationaleMessage != 0) {
|
||||
// display the dialog with the info text
|
||||
AlertDialog.Builder(activity)
|
||||
.setTitle(R.string.permissions_rationale_popup_title)
|
||||
.setMessage(explanationMessage)
|
||||
.setMessage(rationaleMessage)
|
||||
.setOnCancelListener { Toast.makeText(activity, R.string.missing_permissions_warning, Toast.LENGTH_SHORT).show() }
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
if (permissionsListToBeGranted.isNotEmpty()) {
|
||||
|
|
|
@ -0,0 +1,233 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* 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 im.vector.riotx.features.attachments
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.os.Build
|
||||
import android.util.Pair
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewAnimationUtils
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.AnimationSet
|
||||
import android.view.animation.OvershootInterpolator
|
||||
import android.view.animation.ScaleAnimation
|
||||
import android.view.animation.TranslateAnimation
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.PopupWindow
|
||||
import androidx.core.view.doOnNextLayout
|
||||
import com.amulyakhare.textdrawable.TextDrawable
|
||||
import com.amulyakhare.textdrawable.util.ColorGenerator
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.getMeasurements
|
||||
import im.vector.riotx.core.utils.PERMISSIONS_EMPTY
|
||||
import im.vector.riotx.core.utils.PERMISSIONS_FOR_PICKING_CONTACT
|
||||
import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
|
||||
import kotlin.math.max
|
||||
|
||||
private const val ANIMATION_DURATION = 250
|
||||
|
||||
/**
|
||||
* This class is the view presenting choices for picking attachments.
|
||||
* It will return result through [Callback].
|
||||
*/
|
||||
class AttachmentTypeSelectorView(context: Context,
|
||||
inflater: LayoutInflater,
|
||||
var callback: Callback?)
|
||||
: PopupWindow(context) {
|
||||
|
||||
interface Callback {
|
||||
fun onTypeSelected(type: Type)
|
||||
}
|
||||
|
||||
private val iconColorGenerator = ColorGenerator.MATERIAL
|
||||
|
||||
private var galleryButton: ImageButton
|
||||
private var cameraButton: ImageButton
|
||||
private var fileButton: ImageButton
|
||||
private var stickersButton: ImageButton
|
||||
private var audioButton: ImageButton
|
||||
private var contactButton: ImageButton
|
||||
|
||||
private var anchor: View? = null
|
||||
|
||||
init {
|
||||
val root = FrameLayout(context)
|
||||
val layout = inflater.inflate(R.layout.view_attachment_type_selector, root, true)
|
||||
galleryButton = layout.findViewById<ImageButton>(R.id.attachmentGalleryButton).configure(Type.GALLERY)
|
||||
cameraButton = layout.findViewById<ImageButton>(R.id.attachmentCameraButton).configure(Type.CAMERA)
|
||||
fileButton = layout.findViewById<ImageButton>(R.id.attachmentFileButton).configure(Type.FILE)
|
||||
stickersButton = layout.findViewById<ImageButton>(R.id.attachmentStickersButton).configure(Type.STICKER)
|
||||
audioButton = layout.findViewById<ImageButton>(R.id.attachmentAudioButton).configure(Type.AUDIO)
|
||||
contactButton = layout.findViewById<ImageButton>(R.id.attachmentContactButton).configure(Type.CONTACT)
|
||||
contentView = root
|
||||
width = LinearLayout.LayoutParams.MATCH_PARENT
|
||||
height = LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
animationStyle = 0
|
||||
@Suppress("DEPRECATION")
|
||||
setBackgroundDrawable(BitmapDrawable())
|
||||
inputMethodMode = INPUT_METHOD_NOT_NEEDED
|
||||
isFocusable = true
|
||||
isTouchable = true
|
||||
}
|
||||
|
||||
fun show(anchor: View, isKeyboardOpen: Boolean) {
|
||||
this.anchor = anchor
|
||||
val anchorCoordinates = IntArray(2)
|
||||
anchor.getLocationOnScreen(anchorCoordinates)
|
||||
if (isKeyboardOpen) {
|
||||
showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1] + anchor.height)
|
||||
} else {
|
||||
val contentViewHeight = if (contentView.height == 0) {
|
||||
contentView.getMeasurements().second
|
||||
} else {
|
||||
contentView.height
|
||||
}
|
||||
showAtLocation(anchor, Gravity.NO_GRAVITY, 0, anchorCoordinates[1] - contentViewHeight)
|
||||
}
|
||||
contentView.doOnNextLayout {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
animateWindowInCircular(anchor, contentView)
|
||||
} else {
|
||||
animateWindowInTranslate(contentView)
|
||||
}
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
animateButtonIn(galleryButton, ANIMATION_DURATION / 2)
|
||||
animateButtonIn(cameraButton, ANIMATION_DURATION / 2)
|
||||
animateButtonIn(fileButton, ANIMATION_DURATION / 4)
|
||||
animateButtonIn(audioButton, ANIMATION_DURATION / 2)
|
||||
animateButtonIn(contactButton, ANIMATION_DURATION / 4)
|
||||
animateButtonIn(stickersButton, 0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun dismiss() {
|
||||
val capturedAnchor = anchor
|
||||
if (capturedAnchor != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
animateWindowOutCircular(capturedAnchor, contentView)
|
||||
} else {
|
||||
animateWindowOutTranslate(contentView)
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateButtonIn(button: View, delay: Int) {
|
||||
val animation = AnimationSet(true)
|
||||
val scale = ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.0f)
|
||||
animation.addAnimation(scale)
|
||||
animation.interpolator = OvershootInterpolator(1f)
|
||||
animation.duration = ANIMATION_DURATION.toLong()
|
||||
animation.startOffset = delay.toLong()
|
||||
button.startAnimation(animation)
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
private fun animateWindowInCircular(anchor: View, contentView: View) {
|
||||
val coordinates = getClickCoordinates(anchor, contentView)
|
||||
val animator = ViewAnimationUtils.createCircularReveal(contentView,
|
||||
coordinates.first,
|
||||
coordinates.second,
|
||||
0f,
|
||||
max(contentView.width, contentView.height).toFloat())
|
||||
animator.duration = ANIMATION_DURATION.toLong()
|
||||
animator.start()
|
||||
}
|
||||
|
||||
private fun animateWindowInTranslate(contentView: View) {
|
||||
val animation = TranslateAnimation(0f, 0f, contentView.height.toFloat(), 0f)
|
||||
animation.duration = ANIMATION_DURATION.toLong()
|
||||
getContentView().startAnimation(animation)
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
private fun animateWindowOutCircular(anchor: View, contentView: View) {
|
||||
val coordinates = getClickCoordinates(anchor, contentView)
|
||||
val animator = ViewAnimationUtils.createCircularReveal(getContentView(),
|
||||
coordinates.first,
|
||||
coordinates.second,
|
||||
max(getContentView().width, getContentView().height).toFloat(),
|
||||
0f)
|
||||
|
||||
animator.duration = ANIMATION_DURATION.toLong()
|
||||
animator.addListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
super@AttachmentTypeSelectorView.dismiss()
|
||||
}
|
||||
})
|
||||
animator.start()
|
||||
}
|
||||
|
||||
private fun animateWindowOutTranslate(contentView: View) {
|
||||
val animation = TranslateAnimation(0f, 0f, 0f, (contentView.top + contentView.height).toFloat())
|
||||
animation.duration = ANIMATION_DURATION.toLong()
|
||||
animation.setAnimationListener(object : Animation.AnimationListener {
|
||||
override fun onAnimationStart(animation: Animation) {}
|
||||
|
||||
override fun onAnimationEnd(animation: Animation) {
|
||||
super@AttachmentTypeSelectorView.dismiss()
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(animation: Animation) {}
|
||||
})
|
||||
|
||||
getContentView().startAnimation(animation)
|
||||
}
|
||||
|
||||
private fun getClickCoordinates(anchor: View, contentView: View): Pair<Int, Int> {
|
||||
val anchorCoordinates = IntArray(2)
|
||||
anchor.getLocationOnScreen(anchorCoordinates)
|
||||
val contentCoordinates = IntArray(2)
|
||||
contentView.getLocationOnScreen(contentCoordinates)
|
||||
val x = anchorCoordinates[0] - contentCoordinates[0] + anchor.width / 2
|
||||
val y = anchorCoordinates[1] - contentCoordinates[1]
|
||||
return Pair(x, y)
|
||||
}
|
||||
|
||||
private fun ImageButton.configure(type: Type): ImageButton {
|
||||
this.background = TextDrawable.builder().buildRound("", iconColorGenerator.getColor(type.ordinal))
|
||||
this.setOnClickListener(TypeClickListener(type))
|
||||
return this
|
||||
}
|
||||
|
||||
private inner class TypeClickListener(private val type: Type) : View.OnClickListener {
|
||||
|
||||
override fun onClick(v: View) {
|
||||
dismiss()
|
||||
callback?.onTypeSelected(type)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The all possible types to pick with their required permissions.
|
||||
*/
|
||||
enum class Type(val permissionsBit: Int) {
|
||||
|
||||
CAMERA(PERMISSIONS_EMPTY),
|
||||
GALLERY(PERMISSIONS_FOR_WRITING_FILES),
|
||||
FILE(PERMISSIONS_FOR_WRITING_FILES),
|
||||
STICKER(PERMISSIONS_EMPTY),
|
||||
AUDIO(PERMISSIONS_FOR_WRITING_FILES),
|
||||
CONTACT(PERMISSIONS_FOR_PICKING_CONTACT)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,193 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* 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 im.vector.riotx.features.attachments
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.kbeanie.multipicker.api.Picker.*
|
||||
import com.kbeanie.multipicker.core.PickerManager
|
||||
import im.vector.matrix.android.BuildConfig
|
||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||
import im.vector.riotx.core.platform.Restorable
|
||||
import timber.log.Timber
|
||||
|
||||
private const val CAPTURE_PATH_KEY = "CAPTURE_PATH_KEY"
|
||||
private const val PENDING_TYPE_KEY = "PENDING_TYPE_KEY"
|
||||
|
||||
/**
|
||||
* This class helps to handle attachments by providing simple methods.
|
||||
* The process is asynchronous and you must implement [Callback] methods to get the data or a failure.
|
||||
*/
|
||||
class AttachmentsHelper private constructor(private val context: Context,
|
||||
private val pickerManagerFactory: PickerManagerFactory) : Restorable {
|
||||
|
||||
companion object {
|
||||
fun create(fragment: Fragment, callback: Callback): AttachmentsHelper {
|
||||
return AttachmentsHelper(fragment.requireContext(), FragmentPickerManagerFactory(fragment, callback))
|
||||
}
|
||||
|
||||
fun create(activity: Activity, callback: Callback): AttachmentsHelper {
|
||||
return AttachmentsHelper(activity, ActivityPickerManagerFactory(activity, callback))
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onContactAttachmentReady(contactAttachment: ContactAttachment) {
|
||||
if (BuildConfig.LOG_PRIVATE_DATA) {
|
||||
Timber.v("On contact attachment ready: $contactAttachment")
|
||||
}
|
||||
}
|
||||
|
||||
fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>)
|
||||
fun onAttachmentsProcessFailed()
|
||||
}
|
||||
|
||||
// Capture path allows to handle camera image picking. It must be restored if the activity gets killed.
|
||||
private var capturePath: String? = null
|
||||
// The pending type is set if we have to handle permission request. It must be restored if the activity gets killed.
|
||||
var pendingType: AttachmentTypeSelectorView.Type? = null
|
||||
|
||||
private val imagePicker by lazy {
|
||||
pickerManagerFactory.createImagePicker()
|
||||
}
|
||||
|
||||
private val videoPicker by lazy {
|
||||
pickerManagerFactory.createVideoPicker()
|
||||
}
|
||||
|
||||
private val cameraImagePicker by lazy {
|
||||
pickerManagerFactory.createCameraImagePicker()
|
||||
}
|
||||
|
||||
private val filePicker by lazy {
|
||||
pickerManagerFactory.createFilePicker()
|
||||
}
|
||||
|
||||
private val audioPicker by lazy {
|
||||
pickerManagerFactory.createAudioPicker()
|
||||
}
|
||||
|
||||
private val contactPicker by lazy {
|
||||
pickerManagerFactory.createContactPicker()
|
||||
}
|
||||
|
||||
// Restorable
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
capturePath?.also {
|
||||
outState.putString(CAPTURE_PATH_KEY, it)
|
||||
}
|
||||
pendingType?.also {
|
||||
outState.putSerializable(PENDING_TYPE_KEY, it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
|
||||
capturePath = savedInstanceState?.getString(CAPTURE_PATH_KEY)
|
||||
if (capturePath != null) {
|
||||
cameraImagePicker.reinitialize(capturePath)
|
||||
}
|
||||
pendingType = savedInstanceState?.getSerializable(PENDING_TYPE_KEY) as? AttachmentTypeSelectorView.Type
|
||||
}
|
||||
|
||||
// Public Methods
|
||||
|
||||
/**
|
||||
* Starts the process for handling file picking
|
||||
*/
|
||||
fun selectFile() {
|
||||
filePicker.pickFile()
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the process for handling image picking
|
||||
*/
|
||||
fun selectGallery() {
|
||||
imagePicker.pickImage()
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the process for handling audio picking
|
||||
*/
|
||||
fun selectAudio() {
|
||||
audioPicker.pickAudio()
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the process for handling capture image picking
|
||||
*/
|
||||
fun openCamera() {
|
||||
capturePath = cameraImagePicker.pickImage()
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the process for handling contact picking
|
||||
*/
|
||||
fun selectContact() {
|
||||
contactPicker.pickContact()
|
||||
}
|
||||
|
||||
/**
|
||||
* This methods aims to handle on activity result data.
|
||||
*
|
||||
* @return true if it can handle the data, false otherwise
|
||||
*/
|
||||
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
val pickerManager = getPickerManagerForRequestCode(requestCode)
|
||||
if (pickerManager != null) {
|
||||
pickerManager.submit(data)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* This methods aims to handle share intent.
|
||||
*
|
||||
* @return true if it can handle the intent data, false otherwise
|
||||
*/
|
||||
fun handleShareIntent(intent: Intent): Boolean {
|
||||
val type = intent.resolveType(context) ?: return false
|
||||
if (type.startsWith("image")) {
|
||||
imagePicker.submit(intent)
|
||||
} else if (type.startsWith("video")) {
|
||||
videoPicker.submit(intent)
|
||||
} else if (type.startsWith("audio")) {
|
||||
videoPicker.submit(intent)
|
||||
} else if (type.startsWith("application") || type.startsWith("file") || type.startsWith("*")) {
|
||||
filePicker.submit(intent)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getPickerManagerForRequestCode(requestCode: Int): PickerManager? {
|
||||
return when (requestCode) {
|
||||
PICK_IMAGE_DEVICE -> imagePicker
|
||||
PICK_IMAGE_CAMERA -> cameraImagePicker
|
||||
PICK_FILE -> filePicker
|
||||
PICK_CONTACT -> contactPicker
|
||||
PICK_AUDIO -> audioPicker
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* 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 im.vector.riotx.features.attachments
|
||||
|
||||
import com.kbeanie.multipicker.api.entity.*
|
||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||
|
||||
fun ChosenContact.toContactAttachment(): ContactAttachment {
|
||||
return ContactAttachment(
|
||||
displayName = displayName,
|
||||
photoUri = photoUri,
|
||||
emails = emails.toList(),
|
||||
phones = phones.toList()
|
||||
)
|
||||
}
|
||||
|
||||
fun ChosenFile.toContentAttachmentData(): ContentAttachmentData {
|
||||
return ContentAttachmentData(
|
||||
path = originalPath,
|
||||
mimeType = mimeType,
|
||||
type = mapType(),
|
||||
size = size,
|
||||
date = createdAt?.time ?: System.currentTimeMillis(),
|
||||
name = displayName
|
||||
)
|
||||
}
|
||||
|
||||
fun ChosenAudio.toContentAttachmentData(): ContentAttachmentData {
|
||||
return ContentAttachmentData(
|
||||
path = originalPath,
|
||||
mimeType = mimeType,
|
||||
type = mapType(),
|
||||
size = size,
|
||||
date = createdAt?.time ?: System.currentTimeMillis(),
|
||||
name = displayName,
|
||||
duration = duration
|
||||
)
|
||||
}
|
||||
|
||||
fun ChosenFile.mapType(): ContentAttachmentData.Type {
|
||||
return when {
|
||||
mimeType.startsWith("image/") -> ContentAttachmentData.Type.IMAGE
|
||||
mimeType.startsWith("video/") -> ContentAttachmentData.Type.VIDEO
|
||||
mimeType.startsWith("audio/") -> ContentAttachmentData.Type.AUDIO
|
||||
else -> ContentAttachmentData.Type.FILE
|
||||
}
|
||||
}
|
||||
|
||||
fun ChosenImage.toContentAttachmentData(): ContentAttachmentData {
|
||||
return ContentAttachmentData(
|
||||
path = originalPath,
|
||||
mimeType = mimeType,
|
||||
type = mapType(),
|
||||
name = displayName,
|
||||
size = size,
|
||||
height = height.toLong(),
|
||||
width = width.toLong(),
|
||||
exifOrientation = orientation,
|
||||
date = createdAt?.time ?: System.currentTimeMillis()
|
||||
)
|
||||
}
|
||||
|
||||
fun ChosenVideo.toContentAttachmentData(): ContentAttachmentData {
|
||||
return ContentAttachmentData(
|
||||
path = originalPath,
|
||||
mimeType = mimeType,
|
||||
type = ContentAttachmentData.Type.VIDEO,
|
||||
size = size,
|
||||
date = createdAt?.time ?: System.currentTimeMillis(),
|
||||
height = height.toLong(),
|
||||
width = width.toLong(),
|
||||
duration = duration,
|
||||
name = displayName
|
||||
)
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* 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 im.vector.riotx.features.attachments
|
||||
|
||||
import com.kbeanie.multipicker.api.callbacks.AudioPickerCallback
|
||||
import com.kbeanie.multipicker.api.callbacks.ContactPickerCallback
|
||||
import com.kbeanie.multipicker.api.callbacks.FilePickerCallback
|
||||
import com.kbeanie.multipicker.api.callbacks.ImagePickerCallback
|
||||
import com.kbeanie.multipicker.api.callbacks.VideoPickerCallback
|
||||
import com.kbeanie.multipicker.api.entity.*
|
||||
|
||||
/**
|
||||
* This class delegates the PickerManager callbacks to an [AttachmentsHelper.Callback]
|
||||
*/
|
||||
class AttachmentsPickerCallback(private val callback: AttachmentsHelper.Callback)
|
||||
: ImagePickerCallback,
|
||||
FilePickerCallback,
|
||||
VideoPickerCallback,
|
||||
AudioPickerCallback,
|
||||
ContactPickerCallback {
|
||||
|
||||
override fun onContactChosen(contact: ChosenContact?) {
|
||||
if (contact == null) {
|
||||
callback.onAttachmentsProcessFailed()
|
||||
} else {
|
||||
val contactAttachment = contact.toContactAttachment()
|
||||
callback.onContactAttachmentReady(contactAttachment)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAudiosChosen(audios: MutableList<ChosenAudio>?) {
|
||||
if (audios.isNullOrEmpty()) {
|
||||
callback.onAttachmentsProcessFailed()
|
||||
} else {
|
||||
val attachments = audios.map {
|
||||
it.toContentAttachmentData()
|
||||
}
|
||||
callback.onContentAttachmentsReady(attachments)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFilesChosen(files: MutableList<ChosenFile>?) {
|
||||
if (files.isNullOrEmpty()) {
|
||||
callback.onAttachmentsProcessFailed()
|
||||
} else {
|
||||
val attachments = files.map {
|
||||
it.toContentAttachmentData()
|
||||
}
|
||||
callback.onContentAttachmentsReady(attachments)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onImagesChosen(images: MutableList<ChosenImage>?) {
|
||||
if (images.isNullOrEmpty()) {
|
||||
callback.onAttachmentsProcessFailed()
|
||||
} else {
|
||||
val attachments = images.map {
|
||||
it.toContentAttachmentData()
|
||||
}
|
||||
callback.onContentAttachmentsReady(attachments)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onVideosChosen(videos: MutableList<ChosenVideo>?) {
|
||||
if (videos.isNullOrEmpty()) {
|
||||
callback.onAttachmentsProcessFailed()
|
||||
} else {
|
||||
val attachments = videos.map {
|
||||
it.toContentAttachmentData()
|
||||
}
|
||||
callback.onContentAttachmentsReady(attachments)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(error: String?) {
|
||||
callback.onAttachmentsProcessFailed()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* 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 im.vector.riotx.features.attachments
|
||||
|
||||
/**
|
||||
* Data class holding values of a picked contact
|
||||
* Can be send as a text message waiting for the protocol to handle contact.
|
||||
*/
|
||||
data class ContactAttachment(
|
||||
val displayName: String,
|
||||
val photoUri: String?,
|
||||
val phones: List<String> = emptyList(),
|
||||
val emails: List<String> = emptyList()
|
||||
) {
|
||||
|
||||
fun toHumanReadable(): String {
|
||||
return buildString {
|
||||
append(displayName)
|
||||
phones.concatIn(this)
|
||||
emails.concatIn(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<String>.concatIn(stringBuilder: StringBuilder) {
|
||||
if (isNotEmpty()) {
|
||||
stringBuilder.append("\n")
|
||||
for (i in 0 until size - 1) {
|
||||
val value = get(i)
|
||||
stringBuilder.append(value).append("\n")
|
||||
}
|
||||
stringBuilder.append(last())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* 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 im.vector.riotx.features.attachments
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.kbeanie.multipicker.api.AudioPicker
|
||||
import com.kbeanie.multipicker.api.CameraImagePicker
|
||||
import com.kbeanie.multipicker.api.ContactPicker
|
||||
import com.kbeanie.multipicker.api.FilePicker
|
||||
import com.kbeanie.multipicker.api.ImagePicker
|
||||
import com.kbeanie.multipicker.api.VideoPicker
|
||||
|
||||
/**
|
||||
* Factory for creating different pickers. It allows to use with fragment or activity builders.
|
||||
*/
|
||||
interface PickerManagerFactory {
|
||||
|
||||
fun createImagePicker(): ImagePicker
|
||||
|
||||
fun createCameraImagePicker(): CameraImagePicker
|
||||
|
||||
fun createVideoPicker(): VideoPicker
|
||||
|
||||
fun createFilePicker(): FilePicker
|
||||
|
||||
fun createAudioPicker(): AudioPicker
|
||||
|
||||
fun createContactPicker(): ContactPicker
|
||||
}
|
||||
|
||||
class ActivityPickerManagerFactory(private val activity: Activity, callback: AttachmentsHelper.Callback) : PickerManagerFactory {
|
||||
|
||||
private val attachmentsPickerCallback = AttachmentsPickerCallback(callback)
|
||||
|
||||
override fun createImagePicker(): ImagePicker {
|
||||
return ImagePicker(activity).also {
|
||||
it.setImagePickerCallback(attachmentsPickerCallback)
|
||||
it.allowMultiple()
|
||||
}
|
||||
}
|
||||
|
||||
override fun createCameraImagePicker(): CameraImagePicker {
|
||||
return CameraImagePicker(activity).also {
|
||||
it.setImagePickerCallback(attachmentsPickerCallback)
|
||||
}
|
||||
}
|
||||
|
||||
override fun createVideoPicker(): VideoPicker {
|
||||
return VideoPicker(activity).also {
|
||||
it.setVideoPickerCallback(attachmentsPickerCallback)
|
||||
it.allowMultiple()
|
||||
}
|
||||
}
|
||||
|
||||
override fun createFilePicker(): FilePicker {
|
||||
return FilePicker(activity).also {
|
||||
it.allowMultiple()
|
||||
it.setFilePickerCallback(attachmentsPickerCallback)
|
||||
}
|
||||
}
|
||||
|
||||
override fun createAudioPicker(): AudioPicker {
|
||||
return AudioPicker(activity).also {
|
||||
it.allowMultiple()
|
||||
it.setAudioPickerCallback(attachmentsPickerCallback)
|
||||
}
|
||||
}
|
||||
|
||||
override fun createContactPicker(): ContactPicker {
|
||||
return ContactPicker(activity).also {
|
||||
it.setContactPickerCallback(attachmentsPickerCallback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FragmentPickerManagerFactory(private val fragment: Fragment, callback: AttachmentsHelper.Callback) : PickerManagerFactory {
|
||||
|
||||
private val attachmentsPickerCallback = AttachmentsPickerCallback(callback)
|
||||
|
||||
override fun createImagePicker(): ImagePicker {
|
||||
return ImagePicker(fragment).also {
|
||||
it.setImagePickerCallback(attachmentsPickerCallback)
|
||||
it.allowMultiple()
|
||||
}
|
||||
}
|
||||
|
||||
override fun createCameraImagePicker(): CameraImagePicker {
|
||||
return CameraImagePicker(fragment).also {
|
||||
it.setImagePickerCallback(attachmentsPickerCallback)
|
||||
}
|
||||
}
|
||||
|
||||
override fun createVideoPicker(): VideoPicker {
|
||||
return VideoPicker(fragment).also {
|
||||
it.setVideoPickerCallback(attachmentsPickerCallback)
|
||||
it.allowMultiple()
|
||||
}
|
||||
}
|
||||
|
||||
override fun createFilePicker(): FilePicker {
|
||||
return FilePicker(fragment).also {
|
||||
it.allowMultiple()
|
||||
it.setFilePickerCallback(attachmentsPickerCallback)
|
||||
}
|
||||
}
|
||||
|
||||
override fun createAudioPicker(): AudioPicker {
|
||||
return AudioPicker(fragment).also {
|
||||
it.allowMultiple()
|
||||
it.setAudioPickerCallback(attachmentsPickerCallback)
|
||||
}
|
||||
}
|
||||
|
||||
override fun createContactPicker(): ContactPicker {
|
||||
return ContactPicker(fragment).also {
|
||||
it.setContactPickerCallback(attachmentsPickerCallback)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -130,7 +130,7 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
|
|||
}
|
||||
|
||||
private fun exportKeysManually() {
|
||||
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS)) {
|
||||
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS, R.string.permissions_rationale_msg_keys_backup_export)) {
|
||||
ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
|
||||
override fun onPassphrase(passphrase: String) {
|
||||
showWaitingView()
|
||||
|
|
|
@ -135,7 +135,13 @@ class KeysBackupSetupStep3Fragment : VectorBaseFragment() {
|
|||
}
|
||||
|
||||
dialog.findViewById<View>(R.id.keys_backup_setup_save)?.setOnClickListener {
|
||||
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS)) {
|
||||
val permissionsChecked = checkPermissions(
|
||||
PERMISSIONS_FOR_WRITING_FILES,
|
||||
this,
|
||||
PERMISSION_REQUEST_CODE_EXPORT_KEYS,
|
||||
R.string.permissions_rationale_msg_keys_backup_export
|
||||
)
|
||||
if (permissionsChecked) {
|
||||
exportRecoveryKeyToFile(recoveryKey)
|
||||
}
|
||||
dialog.dismiss()
|
||||
|
|
|
@ -27,7 +27,6 @@ import androidx.core.view.isVisible
|
|||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
import com.airbnb.mvrx.viewModel
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
|
@ -55,11 +54,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
|||
object OpenGroup : Navigation()
|
||||
}
|
||||
|
||||
private val homeActivityViewModel: HomeActivityViewModel by viewModel()
|
||||
private lateinit var navigationViewModel: HomeNavigationViewModel
|
||||
|
||||
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
|
||||
@Inject lateinit var homeActivityViewModelFactory: HomeActivityViewModel.Factory
|
||||
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
|
||||
@Inject lateinit var pushManager: PushersManager
|
||||
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
package im.vector.riotx.features.home.room.detail
|
||||
|
||||
import com.jaiselrahman.filepicker.model.MediaFile
|
||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
|
@ -26,7 +26,7 @@ sealed class RoomDetailActions {
|
|||
|
||||
data class SaveDraft(val draft: String) : RoomDetailActions()
|
||||
data class SendMessage(val text: String, val autoMarkdown: Boolean) : RoomDetailActions()
|
||||
data class SendMedia(val mediaFiles: List<MediaFile>) : RoomDetailActions()
|
||||
data class SendMedia(val attachments: List<ContentAttachmentData>) : RoomDetailActions()
|
||||
data class TimelineEventTurnsVisible(val event: TimelineEvent) : RoomDetailActions()
|
||||
data class TimelineEventTurnsInvisible(val event: TimelineEvent) : RoomDetailActions()
|
||||
data class LoadMoreTimelineEvents(val direction: Timeline.Direction) : RoomDetailActions()
|
||||
|
|
|
@ -52,14 +52,12 @@ import com.github.piasy.biv.BigImageViewer
|
|||
import com.github.piasy.biv.loader.ImageLoader
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.jaiselrahman.filepicker.activity.FilePickerActivity
|
||||
import com.jaiselrahman.filepicker.config.Configurations
|
||||
import com.jaiselrahman.filepicker.model.MediaFile
|
||||
import com.otaliastudios.autocomplete.Autocomplete
|
||||
import com.otaliastudios.autocomplete.AutocompleteCallback
|
||||
import com.otaliastudios.autocomplete.CharPolicy
|
||||
import im.vector.matrix.android.api.permalinks.PermalinkFactory
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.api.session.room.model.message.*
|
||||
|
@ -82,6 +80,11 @@ import im.vector.riotx.core.platform.VectorBaseFragment
|
|||
import im.vector.riotx.core.ui.views.JumpToReadMarkerView
|
||||
import im.vector.riotx.core.ui.views.NotificationAreaView
|
||||
import im.vector.riotx.core.utils.*
|
||||
import im.vector.riotx.core.utils.Debouncer
|
||||
import im.vector.riotx.core.utils.createUIHandler
|
||||
import im.vector.riotx.features.attachments.AttachmentTypeSelectorView
|
||||
import im.vector.riotx.features.attachments.AttachmentsHelper
|
||||
import im.vector.riotx.features.attachments.ContactAttachment
|
||||
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
|
||||
import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
|
||||
import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter
|
||||
|
@ -112,6 +115,7 @@ import im.vector.riotx.features.media.VideoMediaViewerActivity
|
|||
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
||||
import im.vector.riotx.features.reactions.EmojiReactionPickerActivity
|
||||
import im.vector.riotx.features.settings.VectorPreferences
|
||||
import im.vector.riotx.features.share.SharedData
|
||||
import im.vector.riotx.features.themes.ThemeUtils
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.fragment_room_detail.*
|
||||
|
@ -125,20 +129,20 @@ import javax.inject.Inject
|
|||
@Parcelize
|
||||
data class RoomDetailArgs(
|
||||
val roomId: String,
|
||||
val eventId: String? = null
|
||||
val eventId: String? = null,
|
||||
val sharedData: SharedData? = null
|
||||
) : Parcelable
|
||||
|
||||
private const val CAMERA_VALUE_TITLE = "attachment"
|
||||
private const val REQUEST_FILES_REQUEST_CODE = 0
|
||||
private const val TAKE_IMAGE_REQUEST_CODE = 1
|
||||
private const val REACTION_SELECT_REQUEST_CODE = 2
|
||||
private const val REACTION_SELECT_REQUEST_CODE = 0
|
||||
|
||||
class RoomDetailFragment :
|
||||
VectorBaseFragment(),
|
||||
TimelineEventController.Callback,
|
||||
AutocompleteUserPresenter.Callback,
|
||||
VectorInviteView.Callback,
|
||||
JumpToReadMarkerView.Callback {
|
||||
JumpToReadMarkerView.Callback,
|
||||
AttachmentTypeSelectorView.Callback,
|
||||
AttachmentsHelper.Callback {
|
||||
|
||||
companion object {
|
||||
|
||||
|
@ -199,9 +203,12 @@ class RoomDetailFragment :
|
|||
|
||||
private lateinit var actionViewModel: ActionsHandler
|
||||
private lateinit var layoutManager: LinearLayoutManager
|
||||
private lateinit var attachmentsHelper: AttachmentsHelper
|
||||
private lateinit var keyboardStateUtils: KeyboardStateUtils
|
||||
|
||||
@BindView(R.id.composerLayout)
|
||||
lateinit var composerLayout: TextComposerView
|
||||
private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView
|
||||
|
||||
private var lockSendButton = false
|
||||
|
||||
|
@ -212,6 +219,8 @@ class RoomDetailFragment :
|
|||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
actionViewModel = ViewModelProviders.of(requireActivity()).get(ActionsHandler::class.java)
|
||||
attachmentsHelper = AttachmentsHelper.create(this, this).register()
|
||||
keyboardStateUtils = KeyboardStateUtils(requireActivity())
|
||||
setupToolbar(roomToolbar)
|
||||
setupRecyclerView()
|
||||
setupComposer()
|
||||
|
@ -275,6 +284,14 @@ class RoomDetailFragment :
|
|||
roomDetailViewModel.requestLiveData.observeEvent(this) {
|
||||
displayRoomDetailActionResult(it)
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
when (val sharedData = roomDetailArgs.sharedData) {
|
||||
is SharedData.Text -> roomDetailViewModel.process(RoomDetailActions.SendMessage(sharedData.text, false))
|
||||
is SharedData.Attachments -> roomDetailViewModel.process(RoomDetailActions.SendMedia(sharedData.attachmentData))
|
||||
null -> Timber.v("No share data to process")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
@ -404,7 +421,7 @@ class RoomDetailFragment :
|
|||
if (text != composerLayout.composerEditText.text.toString()) {
|
||||
// Ignore update to avoid saving a draft
|
||||
composerLayout.composerEditText.setText(text)
|
||||
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
|
||||
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length ?: 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -423,13 +440,14 @@ class RoomDetailFragment :
|
|||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (resultCode == RESULT_OK && data != null) {
|
||||
val hasBeenHandled = attachmentsHelper.onActivityResult(requestCode, resultCode, data)
|
||||
if (!hasBeenHandled && resultCode == RESULT_OK && data != null) {
|
||||
when (requestCode) {
|
||||
REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data)
|
||||
REACTION_SELECT_REQUEST_CODE -> {
|
||||
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) ?: return
|
||||
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) ?: return
|
||||
val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID)
|
||||
?: return
|
||||
val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT)
|
||||
?: return
|
||||
// TODO check if already reacted with that?
|
||||
roomDetailViewModel.process(RoomDetailActions.SendReaction(eventId, reaction))
|
||||
}
|
||||
|
@ -598,47 +616,27 @@ class RoomDetailFragment :
|
|||
composerLayout.composerRelatedMessageCloseButton.setOnClickListener {
|
||||
roomDetailViewModel.process(RoomDetailActions.ExitSpecialMode(composerLayout.composerEditText.text.toString()))
|
||||
}
|
||||
composerLayout.callback = object : TextComposerView.Callback {
|
||||
override fun onRichContentSelected(contentUri: Uri): Boolean {
|
||||
val shareIntent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
data = contentUri
|
||||
}
|
||||
val isHandled = attachmentsHelper.handleShareIntent(shareIntent)
|
||||
if (!isHandled) {
|
||||
Toast.makeText(requireContext(), R.string.error_handling_incoming_share, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
return isHandled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupAttachmentButton() {
|
||||
composerLayout.attachmentButton.setOnClickListener {
|
||||
val intent = Intent(requireContext(), FilePickerActivity::class.java)
|
||||
intent.putExtra(FilePickerActivity.CONFIGS, Configurations.Builder()
|
||||
.setCheckPermission(true)
|
||||
.setShowFiles(true)
|
||||
.setShowAudios(true)
|
||||
.setSkipZeroSizeFiles(true)
|
||||
.build())
|
||||
startActivityForResult(intent, REQUEST_FILES_REQUEST_CODE)
|
||||
/*
|
||||
val items = ArrayList<DialogListItem>()
|
||||
// Send file
|
||||
items.add(DialogListItem.SendFile)
|
||||
// Send voice
|
||||
|
||||
if (vectorPreferences.isSendVoiceFeatureEnabled()) {
|
||||
items.add(DialogListItem.SendVoice.INSTANCE)
|
||||
if (!::attachmentTypeSelector.isInitialized) {
|
||||
attachmentTypeSelector = AttachmentTypeSelectorView(vectorBaseActivity, vectorBaseActivity.layoutInflater, this)
|
||||
}
|
||||
|
||||
|
||||
// Send sticker
|
||||
//items.add(DialogListItem.SendSticker)
|
||||
// Camera
|
||||
|
||||
//if (vectorPreferences.useNativeCamera()) {
|
||||
items.add(DialogListItem.TakePhoto)
|
||||
items.add(DialogListItem.TakeVideo)
|
||||
//} else {
|
||||
// items.add(DialogListItem.TakePhotoVideo.INSTANCE)
|
||||
// }
|
||||
val adapter = DialogSendItemAdapter(requireContext(), items)
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setAdapter(adapter) { _, position ->
|
||||
onSendChoiceClicked(items[position])
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
*/
|
||||
attachmentTypeSelector.show(composerLayout.attachmentButton, keyboardStateUtils.isKeyboardShowing)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -646,38 +644,6 @@ class RoomDetailFragment :
|
|||
inviteView.callback = this
|
||||
}
|
||||
|
||||
/* private fun onSendChoiceClicked(dialogListItem: DialogListItem) {
|
||||
Timber.v("On send choice clicked: $dialogListItem")
|
||||
when (dialogListItem) {
|
||||
is DialogListItem.SendFile -> {
|
||||
// launchFileIntent
|
||||
}
|
||||
is DialogListItem.SendVoice -> {
|
||||
//launchAudioRecorderIntent()
|
||||
}
|
||||
is DialogListItem.SendSticker -> {
|
||||
//startStickerPickerActivity()
|
||||
}
|
||||
is DialogListItem.TakePhotoVideo ->
|
||||
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) {
|
||||
// launchCamera()
|
||||
}
|
||||
is DialogListItem.TakePhoto ->
|
||||
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA)) {
|
||||
openCamera(requireActivity(), CAMERA_VALUE_TITLE, TAKE_IMAGE_REQUEST_CODE)
|
||||
}
|
||||
is DialogListItem.TakeVideo ->
|
||||
if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA)) {
|
||||
// launchNativeVideoRecorder()
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
private fun handleMediaIntent(data: Intent) {
|
||||
val files: ArrayList<MediaFile> = data.getParcelableArrayListExtra(FilePickerActivity.MEDIA_FILES)
|
||||
roomDetailViewModel.process(RoomDetailActions.SendMedia(files))
|
||||
}
|
||||
|
||||
private fun renderState(state: RoomDetailViewState) {
|
||||
readMarkerHelper.updateWith(state)
|
||||
renderRoomSummary(state)
|
||||
|
@ -941,11 +907,16 @@ class RoomDetailFragment :
|
|||
if (allGranted(grantResults)) {
|
||||
if (requestCode == PERMISSION_REQUEST_CODE_DOWNLOAD_FILE) {
|
||||
val action = roomDetailViewModel.pendingAction
|
||||
|
||||
if (action != null) {
|
||||
roomDetailViewModel.pendingAction = null
|
||||
roomDetailViewModel.process(action)
|
||||
}
|
||||
} else if (requestCode == PERMISSION_REQUEST_CODE_PICK_ATTACHMENT) {
|
||||
val pendingType = attachmentsHelper.pendingType
|
||||
if (pendingType != null) {
|
||||
attachmentsHelper.pendingType = null
|
||||
launchAttachmentProcess(pendingType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1164,21 +1135,21 @@ class RoomDetailFragment :
|
|||
val myDisplayName = session.getUser(session.myUserId)?.displayName
|
||||
if (myDisplayName == text) {
|
||||
// current user
|
||||
if (composerLayout.composerEditText.text.isBlank()) {
|
||||
if (composerLayout.composerEditText.text.isNullOrBlank()) {
|
||||
composerLayout.composerEditText.append(Command.EMOTE.command + " ")
|
||||
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
|
||||
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length ?: 0)
|
||||
// vibrate = true
|
||||
}
|
||||
} else {
|
||||
// another user
|
||||
if (composerLayout.composerEditText.text.isBlank()) {
|
||||
if (composerLayout.composerEditText.text.isNullOrBlank()) {
|
||||
// Ensure displayName will not be interpreted as a Slash command
|
||||
if (text.startsWith("/")) {
|
||||
composerLayout.composerEditText.append("\\")
|
||||
}
|
||||
composerLayout.composerEditText.append(sanitizeDisplayName(text) + ": ")
|
||||
} else {
|
||||
composerLayout.composerEditText.text.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayName(text) + " ")
|
||||
composerLayout.composerEditText.text?.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayName(text) + " ")
|
||||
}
|
||||
|
||||
// vibrate = true
|
||||
|
@ -1227,4 +1198,41 @@ class RoomDetailFragment :
|
|||
override fun onClearReadMarkerClicked() {
|
||||
roomDetailViewModel.process(RoomDetailActions.MarkAllAsRead)
|
||||
}
|
||||
|
||||
// AttachmentTypeSelectorView.Callback
|
||||
|
||||
override fun onTypeSelected(type: AttachmentTypeSelectorView.Type) {
|
||||
if (checkPermissions(type.permissionsBit, this, PERMISSION_REQUEST_CODE_PICK_ATTACHMENT)) {
|
||||
launchAttachmentProcess(type)
|
||||
} else {
|
||||
attachmentsHelper.pendingType = type
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) {
|
||||
when (type) {
|
||||
AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera()
|
||||
AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile()
|
||||
AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery()
|
||||
AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio()
|
||||
AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact()
|
||||
AttachmentTypeSelectorView.Type.STICKER -> vectorBaseActivity.notImplemented("Adding stickers")
|
||||
}
|
||||
}
|
||||
|
||||
// AttachmentsHelper.Callback
|
||||
|
||||
override fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>) {
|
||||
roomDetailViewModel.process(RoomDetailActions.SendMedia(attachments))
|
||||
}
|
||||
|
||||
override fun onAttachmentsProcessFailed() {
|
||||
Toast.makeText(requireContext(), R.string.error_attachment, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
override fun onContactAttachmentReady(contactAttachment: ContactAttachment) {
|
||||
super.onContactAttachmentReady(contactAttachment)
|
||||
val formattedContact = contactAttachment.toHumanReadable()
|
||||
roomDetailViewModel.process(RoomDetailActions.SendMessage(formattedContact, false))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
package im.vector.riotx.features.home.room.detail
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
|
@ -27,7 +26,6 @@ import com.squareup.inject.assisted.AssistedInject
|
|||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.MatrixPatterns
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.isImageMessage
|
||||
import im.vector.matrix.android.api.session.events.model.isTextMessage
|
||||
|
@ -49,8 +47,6 @@ import im.vector.matrix.rx.unwrap
|
|||
import im.vector.riotx.BuildConfig
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.postLiveEvent
|
||||
import im.vector.riotx.core.images.ImageTools
|
||||
import im.vector.riotx.core.intent.getFilenameFromUri
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import im.vector.riotx.core.resources.UserPreferencesProvider
|
||||
import im.vector.riotx.core.utils.LiveEvent
|
||||
|
@ -69,7 +65,6 @@ import java.util.concurrent.TimeUnit
|
|||
class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: RoomDetailViewState,
|
||||
userPreferencesProvider: UserPreferencesProvider,
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
private val imageTools: ImageTools,
|
||||
private val session: Session
|
||||
) : VectorViewModel<RoomDetailViewState>(initialState) {
|
||||
|
||||
|
@ -471,32 +466,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
}
|
||||
|
||||
private fun handleSendMedia(action: RoomDetailActions.SendMedia) {
|
||||
val attachments = action.mediaFiles.map {
|
||||
val pathWithScheme = if (it.path.startsWith("/")) {
|
||||
"file://" + it.path
|
||||
} else {
|
||||
it.path
|
||||
}
|
||||
|
||||
val uri = Uri.parse(pathWithScheme)
|
||||
val nameWithExtension = getFilenameFromUri(null, uri)
|
||||
|
||||
ContentAttachmentData(
|
||||
size = it.size,
|
||||
duration = it.duration,
|
||||
date = it.date,
|
||||
height = it.height,
|
||||
width = it.width,
|
||||
exifOrientation = imageTools.getOrientationForBitmap(uri),
|
||||
name = nameWithExtension ?: it.name,
|
||||
path = it.path,
|
||||
mimeType = it.mimeType,
|
||||
type = ContentAttachmentData.Type.values()[it.mediaType]
|
||||
)
|
||||
}
|
||||
|
||||
val attachments = action.attachments
|
||||
val homeServerCapabilities = session.getHomeServerCapabilities()
|
||||
|
||||
val maxUploadFileSize = homeServerCapabilities.maxUploadFileSize
|
||||
|
||||
if (maxUploadFileSize == HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) {
|
||||
|
@ -505,7 +476,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
|||
} else {
|
||||
when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
|
||||
null -> room.sendMedias(attachments)
|
||||
else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name ?: tooBigFile.path, tooBigFile.size, maxUploadFileSize)))
|
||||
else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name
|
||||
?: tooBigFile.path, tooBigFile.size, maxUploadFileSize)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* 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 im.vector.riotx.features.home.room.detail.composer
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputConnection
|
||||
import androidx.appcompat.widget.AppCompatEditText
|
||||
import androidx.core.view.inputmethod.EditorInfoCompat
|
||||
import androidx.core.view.inputmethod.InputConnectionCompat
|
||||
|
||||
class ComposerEditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = android.R.attr.editTextStyle)
|
||||
: AppCompatEditText(context, attrs, defStyleAttr) {
|
||||
|
||||
interface Callback {
|
||||
fun onRichContentSelected(contentUri: Uri): Boolean
|
||||
}
|
||||
|
||||
var callback: Callback? = null
|
||||
|
||||
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection {
|
||||
val ic: InputConnection = super.onCreateInputConnection(editorInfo)
|
||||
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("*/*"))
|
||||
|
||||
val callback =
|
||||
InputConnectionCompat.OnCommitContentListener { inputContentInfo, flags, _ ->
|
||||
val lacksPermission = (flags and
|
||||
InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && lacksPermission) {
|
||||
try {
|
||||
inputContentInfo.requestPermission()
|
||||
} catch (e: Exception) {
|
||||
return@OnCommitContentListener false
|
||||
}
|
||||
}
|
||||
callback?.onRichContentSelected(inputContentInfo.contentUri) ?: false
|
||||
}
|
||||
return InputConnectionCompat.createWrapper(ic, editorInfo, callback)
|
||||
}
|
||||
}
|
|
@ -17,9 +17,9 @@
|
|||
package im.vector.riotx.features.home.room.detail.composer
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
|
@ -39,6 +39,10 @@ import im.vector.riotx.R
|
|||
class TextComposerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
interface Callback : ComposerEditText.Callback
|
||||
|
||||
var callback: Callback? = null
|
||||
|
||||
@BindView(R.id.composer_related_message_sender)
|
||||
lateinit var composerRelatedMessageTitle: TextView
|
||||
@BindView(R.id.composer_related_message_preview)
|
||||
|
@ -50,11 +54,11 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
|
|||
@BindView(R.id.composer_related_message_close)
|
||||
lateinit var composerRelatedMessageCloseButton: ImageButton
|
||||
@BindView(R.id.composerEditText)
|
||||
lateinit var composerEditText: EditText
|
||||
lateinit var composerEditText: ComposerEditText
|
||||
@BindView(R.id.composer_avatar_view)
|
||||
lateinit var composerAvatarImageView: ImageView
|
||||
|
||||
var currentConstraintSetId: Int = -1
|
||||
private var currentConstraintSetId: Int = -1
|
||||
|
||||
private val animationDuration = 100L
|
||||
|
||||
|
@ -62,6 +66,11 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib
|
|||
inflate(context, R.layout.merge_composer_layout, this)
|
||||
ButterKnife.bind(this)
|
||||
collapse(false)
|
||||
composerEditText.callback = object : Callback, ComposerEditText.Callback {
|
||||
override fun onRichContentSelected(contentUri: Uri): Boolean {
|
||||
return callback?.onRichContentSelected(contentUri) ?: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun collapse(animate: Boolean = true, transitionComplete: (() -> Unit)? = null) {
|
||||
|
|
|
@ -42,9 +42,7 @@ class FilteredRoomsActivity : VectorBaseActivity() {
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
configureToolbar(filteredRoomsToolbar)
|
||||
|
||||
if (isFirstCreation()) {
|
||||
roomListFragment = RoomListFragment.newInstance(RoomListParams(RoomListFragment.DisplayMode.FILTERED))
|
||||
replaceFragment(roomListFragment, R.id.filteredRoomsFragmentContainer, FRAGMENT_TAG)
|
||||
|
@ -58,12 +56,10 @@ class FilteredRoomsActivity : VectorBaseActivity() {
|
|||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String): Boolean {
|
||||
// TODO Create a viewModel and remove this public fun
|
||||
roomListFragment.filterRoomsWith(newText)
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
// Open the keyboard immediately
|
||||
filteredRoomsSearchView.requestFocus()
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ class RoomListDisplayModeFilter(private val displayMode: RoomListFragment.Displa
|
|||
RoomListFragment.DisplayMode.PEOPLE -> roomSummary.isDirect && roomSummary.membership == Membership.JOIN
|
||||
RoomListFragment.DisplayMode.ROOMS -> !roomSummary.isDirect && roomSummary.membership == Membership.JOIN
|
||||
RoomListFragment.DisplayMode.FILTERED -> roomSummary.membership == Membership.JOIN
|
||||
RoomListFragment.DisplayMode.SHARE -> roomSummary.membership == Membership.JOIN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,13 +41,15 @@ import im.vector.riotx.core.platform.StateView
|
|||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import im.vector.riotx.features.home.room.list.widget.FabMenuView
|
||||
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
||||
import im.vector.riotx.features.share.SharedData
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.fragment_room_list.*
|
||||
import javax.inject.Inject
|
||||
|
||||
@Parcelize
|
||||
data class RoomListParams(
|
||||
val displayMode: RoomListFragment.DisplayMode
|
||||
val displayMode: RoomListFragment.DisplayMode,
|
||||
val sharedData: SharedData? = null
|
||||
) : Parcelable
|
||||
|
||||
class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, OnBackPressed, FabMenuView.Listener {
|
||||
|
@ -56,7 +58,8 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
|
|||
HOME(R.string.bottom_action_home),
|
||||
PEOPLE(R.string.bottom_action_people_x),
|
||||
ROOMS(R.string.bottom_action_rooms),
|
||||
FILTERED(/* Not used */ R.string.bottom_action_rooms)
|
||||
FILTERED(/* Not used */ 0),
|
||||
SHARE(/* Not used */ 0)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -106,8 +109,13 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
|
|||
setupRecyclerView()
|
||||
roomListViewModel.subscribe { renderState(it) }
|
||||
roomListViewModel.openRoomLiveData.observeEventFirstThrottle(this, 800L) {
|
||||
if (roomListParams.displayMode == DisplayMode.SHARE) {
|
||||
val sharedData = roomListParams.sharedData ?: return@observeEventFirstThrottle
|
||||
navigator.openRoomForSharing(requireActivity(), it, sharedData)
|
||||
} else {
|
||||
navigator.openRoom(requireActivity(), it)
|
||||
}
|
||||
}
|
||||
|
||||
createChatFabMenu.listener = this
|
||||
|
||||
|
@ -124,7 +132,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
|
|||
DisplayMode.HOME -> createChatFabMenu.isVisible = true
|
||||
DisplayMode.PEOPLE -> createChatRoomButton.isVisible = true
|
||||
DisplayMode.ROOMS -> createGroupRoomButton.isVisible = true
|
||||
DisplayMode.FILTERED -> Unit // No button in this mode
|
||||
else -> Unit // No button in this mode
|
||||
}
|
||||
|
||||
createChatRoomButton.setOnClickListener {
|
||||
|
@ -150,7 +158,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
|
|||
DisplayMode.HOME -> createChatFabMenu.hide()
|
||||
DisplayMode.PEOPLE -> createChatRoomButton.hide()
|
||||
DisplayMode.ROOMS -> createGroupRoomButton.hide()
|
||||
DisplayMode.FILTERED -> Unit
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -190,7 +198,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
|
|||
DisplayMode.HOME -> createChatFabMenu.show()
|
||||
DisplayMode.PEOPLE -> createChatRoomButton.show()
|
||||
DisplayMode.ROOMS -> createGroupRoomButton.show()
|
||||
DisplayMode.FILTERED -> Unit
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -261,7 +269,7 @@ class RoomListFragment : VectorBaseFragment(), RoomSummaryController.Listener, O
|
|||
ContextCompat.getDrawable(requireContext(), R.drawable.ic_home_bottom_group),
|
||||
getString(R.string.room_list_rooms_empty_body)
|
||||
)
|
||||
DisplayMode.FILTERED ->
|
||||
else ->
|
||||
// Always display the content in this mode, because if the footer
|
||||
StateView.State.Content
|
||||
}
|
||||
|
|
|
@ -230,6 +230,7 @@ class RoomListViewModel @AssistedInject constructor(@Assisted initialState: Room
|
|||
RoomListFragment.DisplayMode.PEOPLE -> chronologicalRoomComparator
|
||||
RoomListFragment.DisplayMode.ROOMS -> chronologicalRoomComparator
|
||||
RoomListFragment.DisplayMode.FILTERED -> chronologicalRoomComparator
|
||||
RoomListFragment.DisplayMode.SHARE -> chronologicalRoomComparator
|
||||
}
|
||||
|
||||
return RoomSummaries().apply {
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package im.vector.riotx.features.navigation
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
|
||||
|
@ -33,6 +34,7 @@ import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
|
|||
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
|
||||
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewActivity
|
||||
import im.vector.riotx.features.settings.VectorSettingsActivity
|
||||
import im.vector.riotx.features.share.SharedData
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
@ -46,6 +48,13 @@ class DefaultNavigator @Inject constructor() : Navigator {
|
|||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
override fun openRoomForSharing(activity: Activity, roomId: String, sharedData: SharedData) {
|
||||
val args = RoomDetailArgs(roomId, null, sharedData)
|
||||
val intent = RoomDetailActivity.newIntent(activity, args)
|
||||
activity.startActivity(intent)
|
||||
activity.finish()
|
||||
}
|
||||
|
||||
override fun openNotJoinedRoom(context: Context, roomIdOrAlias: String, eventId: String?) {
|
||||
if (context is VectorBaseActivity) {
|
||||
context.notImplemented("Open not joined room")
|
||||
|
|
|
@ -16,13 +16,17 @@
|
|||
|
||||
package im.vector.riotx.features.navigation
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
|
||||
import im.vector.riotx.features.share.SharedData
|
||||
|
||||
interface Navigator {
|
||||
|
||||
fun openRoom(context: Context, roomId: String, eventId: String? = null)
|
||||
|
||||
fun openRoomForSharing(activity: Activity, roomId: String, sharedData: SharedData)
|
||||
|
||||
fun openNotJoinedRoom(context: Context, roomIdOrAlias: String, eventId: String? = null)
|
||||
|
||||
fun openRoomPreview(publicRoom: PublicRoom, context: Context)
|
||||
|
|
|
@ -211,7 +211,7 @@ class VectorSettingsSecurityPrivacyFragment : VectorSettingsBaseFragment() {
|
|||
*/
|
||||
private fun exportKeys() {
|
||||
// We need WRITE_EXTERNAL permission
|
||||
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS)) {
|
||||
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS, R.string.permissions_rationale_msg_keys_backup_export)) {
|
||||
activity?.let { activity ->
|
||||
ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener {
|
||||
override fun onPassphrase(passphrase: String) {
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* 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 im.vector.riotx.features.share
|
||||
|
||||
import android.content.ClipDescription
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import com.kbeanie.multipicker.utils.IntentUtils
|
||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.extensions.replaceFragment
|
||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||
import im.vector.riotx.features.attachments.AttachmentsHelper
|
||||
import im.vector.riotx.features.home.LoadingFragment
|
||||
import im.vector.riotx.features.home.room.list.RoomListFragment
|
||||
import im.vector.riotx.features.home.room.list.RoomListParams
|
||||
import im.vector.riotx.features.login.LoginActivity
|
||||
import kotlinx.android.synthetic.main.activity_incoming_share.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class IncomingShareActivity :
|
||||
VectorBaseActivity(), AttachmentsHelper.Callback {
|
||||
|
||||
@Inject lateinit var sessionHolder: ActiveSessionHolder
|
||||
private lateinit var roomListFragment: RoomListFragment
|
||||
private lateinit var attachmentsHelper: AttachmentsHelper
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.activity_incoming_share
|
||||
}
|
||||
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// If we are not logged in, stop the sharing process and open login screen.
|
||||
// In the future, we might want to relaunch the sharing process after login.
|
||||
if (!sessionHolder.hasActiveSession()) {
|
||||
startLoginActivity()
|
||||
return
|
||||
}
|
||||
configureToolbar(incomingShareToolbar)
|
||||
if (isFirstCreation()) {
|
||||
val loadingDetail = LoadingFragment.newInstance()
|
||||
replaceFragment(loadingDetail, R.id.shareRoomListFragmentContainer)
|
||||
}
|
||||
attachmentsHelper = AttachmentsHelper.create(this, this).register()
|
||||
if (intent?.action == Intent.ACTION_SEND || intent?.action == Intent.ACTION_SEND_MULTIPLE) {
|
||||
var isShareManaged = attachmentsHelper.handleShareIntent(
|
||||
IntentUtils.getPickerIntentForSharing(intent)
|
||||
)
|
||||
if (!isShareManaged) {
|
||||
isShareManaged = handleTextShare(intent)
|
||||
}
|
||||
if (!isShareManaged) {
|
||||
cannotManageShare()
|
||||
}
|
||||
} else {
|
||||
cannotManageShare()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onContentAttachmentsReady(attachments: List<ContentAttachmentData>) {
|
||||
val roomListParams = RoomListParams(RoomListFragment.DisplayMode.SHARE, sharedData = SharedData.Attachments(attachments))
|
||||
roomListFragment = RoomListFragment.newInstance(roomListParams)
|
||||
replaceFragment(roomListFragment, R.id.shareRoomListFragmentContainer)
|
||||
}
|
||||
|
||||
override fun onAttachmentsProcessFailed() {
|
||||
cannotManageShare()
|
||||
}
|
||||
|
||||
private fun cannotManageShare() {
|
||||
Toast.makeText(this, R.string.error_handling_incoming_share, Toast.LENGTH_LONG).show()
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun handleTextShare(intent: Intent): Boolean {
|
||||
if (intent.type == ClipDescription.MIMETYPE_TEXT_PLAIN) {
|
||||
val sharedText = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)?.toString()
|
||||
return if (sharedText.isNullOrEmpty()) {
|
||||
false
|
||||
} else {
|
||||
val roomListParams = RoomListParams(RoomListFragment.DisplayMode.SHARE, sharedData = SharedData.Text(sharedText))
|
||||
roomListFragment = RoomListFragment.newInstance(roomListParams)
|
||||
replaceFragment(roomListFragment, R.id.shareRoomListFragmentContainer)
|
||||
true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun startLoginActivity() {
|
||||
val intent = LoginActivity.newIntent(this, null)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright 2019 New Vector Ltd
|
||||
*
|
||||
* 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 im.vector.riotx.features.share
|
||||
|
||||
import android.os.Parcelable
|
||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
sealed class SharedData: Parcelable {
|
||||
|
||||
@Parcelize
|
||||
data class Text(val text: String): SharedData()
|
||||
|
||||
@Parcelize
|
||||
data class Attachments(val attachmentData: List<ContentAttachmentData>): SharedData()
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 707 B |
Binary file not shown.
After Width: | Height: | Size: 442 B |
Binary file not shown.
After Width: | Height: | Size: 928 B |
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.9 KiB |
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="?riotx_attachment_selector_border" />
|
||||
<corners android:radius="16dp" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<item
|
||||
android:bottom="1dp"
|
||||
android:left="1dp"
|
||||
android:right="1dp"
|
||||
android:top="1dp">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="?riotx_attachment_selector_background" />
|
||||
<corners android:radius="16dp" />
|
||||
|
||||
</shape>
|
||||
|
||||
</item>
|
||||
|
||||
|
||||
</layer-list>
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp">
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M12,1c-4.97,0 -9,4.03 -9,9v7c0,1.66 1.34,3 3,3h3v-8H5v-2c0,-3.87 3.13,-7 7,-7s7,3.13 7,7v2h-4v8h3c1.66,0 3,-1.34 3,-3v-7c0,-4.97 -4.03,-9 -9,-9z"/>
|
||||
</vector>
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp">
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M9.4,10.5l4.77,-8.26C13.47,2.09 12.75,2 12,2c-2.4,0 -4.6,0.85 -6.32,2.25l3.66,6.35 0.06,-0.1zM21.54,9c-0.92,-2.92 -3.15,-5.26 -6,-6.34L11.88,9h9.66zM21.8,10h-7.49l0.29,0.5 4.76,8.25C21,16.97 22,14.61 22,12c0,-0.69 -0.07,-1.35 -0.2,-2zM8.54,12l-3.9,-6.75C3.01,7.03 2,9.39 2,12c0,0.69 0.07,1.35 0.2,2h7.49l-1.15,-2zM2.46,15c0.92,2.92 3.15,5.26 6,6.34L12.12,15L2.46,15zM13.73,15l-3.9,6.76c0.7,0.15 1.42,0.24 2.17,0.24 2.4,0 4.6,-0.85 6.32,-2.25l-3.66,-6.35 -0.93,1.6z"/>
|
||||
</vector>
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp">
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M16,11c1.66,0 2.99,-1.34 2.99,-3S17.66,5 16,5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3zM8,11c1.66,0 2.99,-1.34 2.99,-3S9.66,5 8,5C6.34,5 5,6.34 5,8s1.34,3 3,3zM8,13c-2.33,0 -7,1.17 -7,3.5L1,19h14v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5zM16,13c-0.29,0 -0.62,0.02 -0.97,0.05 1.16,0.84 1.97,1.97 1.97,3.45L17,19h6v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5z"/>
|
||||
</vector>
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp">
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6L6,2zM13,9L13,3.5L18.5,9L13,9z"/>
|
||||
</vector>
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp">
|
||||
<path android:fillColor="#FFFFFF" android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
|
||||
</vector>
|
|
@ -0,0 +1,43 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout 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">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/incomingShareToolbar"
|
||||
style="@style/VectorToolbarStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:elevation="4dp"
|
||||
app:contentInsetStart="0dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<androidx.appcompat.widget.SearchView
|
||||
android:id="@+id/incomingShareSearchView"
|
||||
style="@style/VectorSearchView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:queryHint="@string/room_filtering_filter_hint"
|
||||
app:searchIcon="@drawable/ic_filter" />
|
||||
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/shareRoomListFragmentContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/incomingShareToolbar" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -143,7 +143,7 @@
|
|||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/attachmentButton" />
|
||||
|
||||
<EditText
|
||||
<im.vector.riotx.features.home.room.detail.composer.ComposerEditText
|
||||
android:id="@+id/composerEditText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
@ -153,7 +153,7 @@
|
|||
app:layout_constraintTop_toBottomOf="@id/composer_preview_barrier"
|
||||
app:layout_constraintVertical_bias="1" />
|
||||
|
||||
<EditText
|
||||
<im.vector.riotx.features.home.room.detail.composer.ComposerEditText
|
||||
android:id="@+id/composerEditText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
@ -114,7 +114,7 @@
|
|||
android:tint="?attr/colorAccent"
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
||||
<EditText
|
||||
<im.vector.riotx.features.home.room.detail.composer.ComposerEditText
|
||||
android:id="@+id/composerEditText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
<?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="@drawable/bg_attachment_type_selector"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="16dp"
|
||||
android:baselineAligned="false"
|
||||
android:orientation="horizontal"
|
||||
android:weightSum="3">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/attachmentCameraButton"
|
||||
style="@style/AttachmentTypeSelectorButton"
|
||||
android:src="@drawable/ic_attachment_camera_white_24dp"
|
||||
tools:background="@color/colorAccent" />
|
||||
|
||||
<TextView
|
||||
style="@style/AttachmentTypeSelectorLabel"
|
||||
android:text="@string/attachment_type_camera" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/attachmentGalleryButton"
|
||||
style="@style/AttachmentTypeSelectorButton"
|
||||
android:src="@drawable/ic_attachment_gallery_white_24dp"
|
||||
tools:background="@color/colorAccent" />
|
||||
|
||||
<TextView
|
||||
style="@style/AttachmentTypeSelectorLabel"
|
||||
android:text="@string/attachment_type_gallery" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/attachmentFileButton"
|
||||
style="@style/AttachmentTypeSelectorButton"
|
||||
android:src="@drawable/ic_attachment_file_white_24dp"
|
||||
tools:background="@color/colorAccent" />
|
||||
|
||||
<TextView
|
||||
style="@style/AttachmentTypeSelectorLabel"
|
||||
android:text="@string/attachment_type_file" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="16dp"
|
||||
android:baselineAligned="false"
|
||||
android:orientation="horizontal"
|
||||
android:weightSum="3">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/attachmentAudioButton"
|
||||
style="@style/AttachmentTypeSelectorButton"
|
||||
android:src="@drawable/ic_attachment_audio_white_24dp"
|
||||
tools:background="@color/colorAccent" />
|
||||
|
||||
<TextView
|
||||
style="@style/AttachmentTypeSelectorLabel"
|
||||
android:text="@string/attachment_type_audio" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/attachmentContactButton"
|
||||
style="@style/AttachmentTypeSelectorButton"
|
||||
android:src="@drawable/ic_attachment_contact_white_24dp"
|
||||
tools:background="@color/colorAccent" />
|
||||
|
||||
<TextView
|
||||
style="@style/AttachmentTypeSelectorLabel"
|
||||
android:text="@string/attachment_type_contact" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/attachmentStickersButton"
|
||||
style="@style/AttachmentTypeSelectorButton"
|
||||
android:src="@drawable/ic_attachment_stickers_white_24dp"
|
||||
tools:background="@color/colorAccent" />
|
||||
|
||||
<TextView
|
||||
style="@style/AttachmentTypeSelectorLabel"
|
||||
android:text="@string/attachment_type_sticker" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
|
@ -147,6 +147,16 @@
|
|||
<color name="riotx_touch_guard_bg_dark">#BF000000</color>
|
||||
<color name="riotx_touch_guard_bg_black">#BF000000</color>
|
||||
|
||||
<attr name="riotx_attachment_selector_background" format="color" />
|
||||
<color name="riotx_attachment_selector_background_light">#FFFFFFFF</color>
|
||||
<color name="riotx_attachment_selector_background_dark">#FF22262E</color>
|
||||
<color name="riotx_attachment_selector_background_black">#FF090A0C</color>
|
||||
|
||||
<attr name="riotx_attachment_selector_border" format="color" />
|
||||
<color name="riotx_attachment_selector_border_light">#FFE9EDF1</color>
|
||||
<color name="riotx_attachment_selector_border_dark">#FF22262E</color>
|
||||
<color name="riotx_attachment_selector_border_black">#FF090A0C</color>
|
||||
|
||||
<!-- (color from RiotWeb) -->
|
||||
<attr name="riotx_keys_backup_banner_accent_color" format="color" />
|
||||
<color name="riotx_keys_backup_banner_accent_color_light">#FFF8E3</color>
|
||||
|
|
|
@ -369,6 +369,9 @@
|
|||
<string name="permissions_rationale_msg_contacts">Riot can check your address book to find other Matrix users based on their email and phone numbers. If you agree to share your address book for this purpose, please allow access on the next pop-up.</string>
|
||||
<string name="permissions_msg_contacts_warning_other_androids">Riot can check your address book to find other Matrix users based on their email and phone numbers.\n\nDo you agree to share your address book for this purpose?</string>
|
||||
|
||||
|
||||
|
||||
|
||||
<string name="permissions_action_not_performed_missing_permissions">Sorry. Action not performed, due to missing permissions</string>
|
||||
|
||||
<!-- medias slider string -->
|
||||
|
|
|
@ -37,6 +37,14 @@
|
|||
|
||||
<string name="error_file_too_big">"The file '%1$s' (%2$s) is too large to upload. The limit is %3$s."</string>
|
||||
|
||||
<string name="error_attachment">"An error occurred while retrieving the attachment."</string>
|
||||
<string name="attachment_type_file">"File"</string>
|
||||
<string name="attachment_type_contact">"Contact"</string>
|
||||
<string name="attachment_type_camera">"Camera"</string>
|
||||
<string name="attachment_type_audio">"Audio"</string>
|
||||
<string name="attachment_type_gallery">"Gallery"</string>
|
||||
<string name="attachment_type_sticker">"Sticker"</string>
|
||||
<string name="error_handling_incoming_share">Couldn\'t handle share data</string>
|
||||
|
||||
<string name="report_content_spam">"It's spam"</string>
|
||||
<string name="report_content_inappropriate">"It's inappropriate"</string>
|
||||
|
@ -52,4 +60,7 @@
|
|||
<string name="content_reported_as_spam_content">"This content was reported as spam.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages"</string>
|
||||
<string name="content_reported_as_inappropriate_title">"Reported as inappropriate"</string>
|
||||
<string name="content_reported_as_inappropriate_content">"This content was reported as inappropriate.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages"</string>
|
||||
|
||||
<string name="permissions_rationale_msg_keys_backup_export">Riot needs permission to save your E2E keys on disk.\n\nPlease allow access on the next pop-up to be able to export your keys manually.</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -324,4 +324,19 @@
|
|||
<item name="android:textSize">12sp</item>
|
||||
</style>
|
||||
|
||||
|
||||
<style name="AttachmentTypeSelectorButton">
|
||||
<item name="android:layout_width">56dp</item>
|
||||
<item name="android:layout_height">56dp</item>
|
||||
<item name="android:scaleType">center</item>
|
||||
</style>
|
||||
|
||||
<style name="AttachmentTypeSelectorLabel">
|
||||
<item name="android:layout_width">wrap_content</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:textColor">?riotx_text_primary</item>
|
||||
<item name="android:textSize">14sp</item>
|
||||
<item name="android:layout_marginTop">8dp</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
|
@ -30,6 +30,8 @@
|
|||
<item name="riotx_fab_label_bg">@color/riotx_fab_label_bg_black</item>
|
||||
<item name="riotx_fab_label_color">@color/riotx_fab_label_color_black</item>
|
||||
<item name="riotx_touch_guard_bg">@color/riotx_touch_guard_bg_black</item>
|
||||
<item name="riotx_attachment_selector_background">@color/riotx_attachment_selector_background_black</item>
|
||||
<item name="riotx_attachment_selector_border">@color/riotx_attachment_selector_border_black</item>
|
||||
|
||||
<!-- Drawables -->
|
||||
<item name="riotx_highlighted_message_background">@drawable/highlighted_message_background_black</item>
|
||||
|
|
|
@ -28,6 +28,8 @@
|
|||
<item name="riotx_fab_label_bg">@color/riotx_fab_label_bg_dark</item>
|
||||
<item name="riotx_fab_label_color">@color/riotx_fab_label_color_dark</item>
|
||||
<item name="riotx_touch_guard_bg">@color/riotx_touch_guard_bg_dark</item>
|
||||
<item name="riotx_attachment_selector_background">@color/riotx_attachment_selector_background_dark</item>
|
||||
<item name="riotx_attachment_selector_border">@color/riotx_attachment_selector_border_dark</item>
|
||||
|
||||
<item name="riotx_keys_backup_banner_accent_color">@color/riotx_keys_backup_banner_accent_color_dark</item>
|
||||
|
||||
|
|
|
@ -29,6 +29,8 @@
|
|||
<item name="riotx_fab_label_color">@color/riotx_fab_label_color_light</item>
|
||||
<item name="riotx_touch_guard_bg">@color/riotx_touch_guard_bg_light</item>
|
||||
<item name="riotx_keys_backup_banner_accent_color">@color/riotx_keys_backup_banner_accent_color_light</item>
|
||||
<item name="riotx_attachment_selector_background">@color/riotx_attachment_selector_background_light</item>
|
||||
<item name="riotx_attachment_selector_border">@color/riotx_attachment_selector_border_light</item>
|
||||
|
||||
<!-- Drawables -->
|
||||
<item name="riotx_highlighted_message_background">@drawable/highlighted_message_background_light</item>
|
||||
|
|
Loading…
Reference in New Issue