Skip to content
Snippets Groups Projects
Commit c54106ed authored by Taras's avatar Taras
Browse files

Merge branch 'feature/video_playback' into develop

parents fe0555db d82e328d
No related branches found
No related tags found
No related merge requests found
Showing
with 232 additions and 77 deletions
......@@ -66,7 +66,7 @@ body:
label: Homeserver
description: |
Which server is your account registered on?
placeholder: e.g. matrix.org or circu.li
placeholder: e.g. matrix.org or circles.futo.org
validations:
required: false
- type: input
......
blank_issues_enabled: false
contact_links:
- name: Circles web
url: https://circu.li/contact.html
url: https://circles.futo.org
about: You can find our contact info here.
- name: Circles Matrix room
url: https://matrix.to/#/!gcbIHWAYBBmvITkQIn:matrix.org?via=matrix.org&via=envs.net&via=tchncs.de
......
# FUTO Circles
[Circles](https://circu.li/circles.html) is an end-to-end encrypted social network app
[Circles](https://circles.futo.org/) is an end-to-end encrypted social network app
that enables friends and families to securely share stories and photos while safeguarding
security and privacy.
......
......@@ -105,7 +105,7 @@ dependencies {
implementation project(path: ':gallery')
//Firebase
gplayImplementation platform('com.google.firebase:firebase-bom:32.7.4')
gplayImplementation platform('com.google.firebase:firebase-bom:32.8.0')
gplayImplementation 'com.google.firebase:firebase-crashlytics-ktx'
gplayImplementation 'com.google.firebase:firebase-analytics-ktx'
gplayImplementation 'com.google.firebase:firebase-messaging-ktx'
......
......@@ -49,7 +49,6 @@
<data android:scheme="https" />
<data android:scheme="http" />
<data android:host="circu.li" />
<data android:host="circles.futo.org" />
<data android:pathPrefix="/room" />
<data android:pathPrefix="/profile" />
......
......@@ -15,7 +15,7 @@ import androidx.recyclerview.widget.DividerItemDecoration
import by.kirich1409.viewbindingdelegate.viewBinding
import dagger.hilt.android.AndroidEntryPoint
import org.futo.circles.R
import org.futo.circles.auth.explanation.CirclesExplanationDialog
import org.futo.circles.auth.feature.explanation.CirclesExplanationDialog
import org.futo.circles.core.base.NetworkObserver
import org.futo.circles.core.databinding.FragmentRoomsBinding
import org.futo.circles.core.extensions.navigateSafe
......
......@@ -35,7 +35,7 @@ class AcceptCircleInviteDialogFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
addSelectCirclesFragment()
if (savedInstanceState == null) addSelectCirclesFragment()
setupViews()
setupObservers()
}
......
......@@ -15,7 +15,7 @@ import androidx.recyclerview.widget.DividerItemDecoration
import by.kirich1409.viewbindingdelegate.viewBinding
import dagger.hilt.android.AndroidEntryPoint
import org.futo.circles.R
import org.futo.circles.auth.explanation.CirclesExplanationDialog
import org.futo.circles.auth.feature.explanation.CirclesExplanationDialog
import org.futo.circles.core.base.NetworkObserver
import org.futo.circles.core.databinding.FragmentRoomsBinding
import org.futo.circles.core.extensions.navigateSafe
......
......@@ -42,15 +42,13 @@ class SelectRoomsDataSource @Inject constructor(
}.flowOn(Dispatchers.IO).distinctUntilChanged()
private fun getRoomsFlowWithType(): Flow<List<RoomSummary>> = when (roomType) {
CircleRoomTypeArg.Circle -> {
CircleRoomTypeArg.Circle -> getSpacesLiveData(listOf(Membership.JOIN)).map { summaries ->
val joinedCirclesIds = circleDataSource.getJoinedCirclesIds()
getSpacesLiveData(listOf(Membership.JOIN)).map { summaries ->
summaries.mapNotNull { summary ->
if (joinedCirclesIds.contains(summary.roomId)) summary
else null
}
}.asFlow()
}
summaries.mapNotNull { summary ->
if (joinedCirclesIds.contains(summary.roomId)) summary
else null
}
}.asFlow()
CircleRoomTypeArg.Group -> getGroupsLiveData(listOf(Membership.JOIN)).asFlow()
CircleRoomTypeArg.Photo -> getGalleriesLiveData(listOf(Membership.JOIN)).asFlow()
......
......@@ -17,7 +17,7 @@ class SettingsDataSource @Inject constructor(
val startReAuthEventLiveData = authConfirmationProvider.startReAuthEventLiveData
suspend fun deactivateAccount(): Response<Unit> = createResult {
session.accountService().deactivateAccount(false, authConfirmationProvider)
session.accountService().deactivateAccount(true, authConfirmationProvider)
}
suspend fun addEmailUIA() = createResult {
......
......@@ -11,6 +11,7 @@ import by.kirich1409.viewbindingdelegate.viewBinding
import dagger.hilt.android.AndroidEntryPoint
import org.futo.circles.MainActivity
import org.futo.circles.R
import org.futo.circles.auth.extensions.openCustomTabUrl
import org.futo.circles.auth.feature.uia.flow.reauth.ReAuthCancellationListener
import org.futo.circles.auth.model.LogOut
import org.futo.circles.auth.model.SwitchUser
......@@ -87,6 +88,7 @@ class SettingsFragment : Fragment(R.layout.fragment_settings), ReAuthCancellatio
tvPushNotifications.setOnClickListener { navigator.navigateToPushSettings() }
tvEditProfile.setOnClickListener { navigator.navigateToEditProfile() }
tvShareProfile.setOnClickListener { navigator.navigateToShareProfile(viewModel.getSharedCircleSpaceId()) }
tvPrivacyPolicy.setOnClickListener { openCustomTabUrl(getString(R.string.privacy_policy_url)) }
}
setVersion()
}
......
......@@ -5,6 +5,9 @@ import android.view.View
import androidx.core.content.ContextCompat
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
......@@ -30,6 +33,7 @@ import org.futo.circles.core.model.CreatePollContent
import org.futo.circles.core.model.PostContent
import org.futo.circles.core.utils.debounce
import org.futo.circles.databinding.DialogFragmentTimelineBinding
import org.futo.circles.feature.timeline.list.PostOptionsListener
import org.futo.circles.feature.timeline.list.TimelineAdapter
import org.futo.circles.feature.timeline.poll.CreatePollListener
import org.futo.circles.feature.timeline.post.create.CreatePostListener
......@@ -40,7 +44,6 @@ import org.futo.circles.model.EndPoll
import org.futo.circles.model.IgnoreSender
import org.futo.circles.model.RemovePost
import org.futo.circles.view.CreatePostViewListener
import org.futo.circles.view.PostOptionsListener
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.powerlevels.Role
......@@ -70,12 +73,14 @@ class TimelineDialogFragment : BaseFullscreenDialogFragment(DialogFragmentTimeli
)
}
private val videoPlayer by lazy {
ExoPlayer.Builder(requireContext()).build().apply {
repeatMode = Player.REPEAT_MODE_ONE
}
}
private val listAdapter by lazy {
TimelineAdapter(
getCurrentUserPowerLevel(args.roomId),
this,
isThread
) { loadMoreDebounce(Unit) }.apply {
TimelineAdapter(this, isThread, videoPlayer) { loadMoreDebounce(Unit) }.apply {
setHasStableIds(true)
registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
......@@ -102,15 +107,29 @@ class TimelineDialogFragment : BaseFullscreenDialogFragment(DialogFragmentTimeli
setupViews()
setupObservers()
setupMenu()
findNavController().addOnDestinationChangedListener { _, destination, _ ->
if (destination.id != R.id.timelineFragment) listAdapter.stopVideoPlayback()
}
}
override fun onPause() {
super.onPause()
listAdapter.stopVideoPlayback()
}
override fun onDestroy() {
videoPlayer.stop()
videoPlayer.release()
super.onDestroy()
}
private fun setupViews() {
binding.rvTimeline.apply {
adapter = listAdapter
getRecyclerView().apply {
isNestedScrollingEnabled = false
setHasFixedSize(true)
setItemViewCacheSize(20)
}
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
}
......@@ -216,6 +235,7 @@ class TimelineDialogFragment : BaseFullscreenDialogFragment(DialogFragmentTimeli
override fun onShowEmoji(roomId: String, eventId: String, onAddEmoji: (String) -> Unit) {
if (showNoInternetConnection()) return
if (showErrorIfNotAbleToPost()) return
onLocalAddEmojiCallback = onAddEmoji
navigator.navigateToShowEmoji(roomId, eventId)
}
......@@ -246,16 +266,14 @@ class TimelineDialogFragment : BaseFullscreenDialogFragment(DialogFragmentTimeli
roomId: String, eventId: String, emoji: String, isUnSend: Boolean
) {
if (showNoInternetConnection()) return
if (viewModel.accessLevelLiveData.value?.isCurrentUserAbleToPost() != true) {
showError(getString(R.string.you_can_not_post_to_this_room))
return
}
if (showErrorIfNotAbleToPost()) return
if (isUnSend) viewModel.unSendReaction(roomId, eventId, emoji)
else viewModel.sendReaction(roomId, eventId, emoji)
}
override fun onPollOptionSelected(roomId: String, eventId: String, optionId: String) {
if (showNoInternetConnection()) return
if (showErrorIfNotAbleToPost()) return
viewModel.pollVote(roomId, eventId, optionId)
}
......@@ -320,7 +338,12 @@ class TimelineDialogFragment : BaseFullscreenDialogFragment(DialogFragmentTimeli
private fun onUserAccessLevelChanged(powerLevelsContent: PowerLevelsContent) {
if (isGroupMode) onGroupUserAccessLevelChanged(powerLevelsContent)
else onCircleUserAccessLeveChanged(powerLevelsContent)
listAdapter.updateUserPowerLevel(getCurrentUserPowerLevel(args.roomId))
}
private fun showErrorIfNotAbleToPost(): Boolean {
val isAbleToPost = viewModel.accessLevelLiveData.value?.isCurrentUserAbleToPost() == true
if (!isAbleToPost) showError(getString(R.string.you_can_not_post_to_this_room))
return !isAbleToPost
}
private fun onGroupUserAccessLevelChanged(powerLevelsContent: PowerLevelsContent) {
......
package org.futo.circles.feature.timeline
import android.content.Context
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.asLiveData
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import org.futo.circles.core.base.SingleEventLiveData
......@@ -32,6 +34,7 @@ import javax.inject.Inject
@HiltViewModel
class TimelineViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
@ApplicationContext context: Context,
roomNotificationsDataSource: RoomNotificationsDataSource,
timelineDataSourceFactory: BaseTimelineDataSource.Factory,
accessLevelDataSource: AccessLevelDataSource,
......@@ -43,6 +46,7 @@ class TimelineViewModel @Inject constructor(
circleFilterAccountDataManager: CircleFilterAccountDataManager
) : BaseTimelineViewModel(
savedStateHandle,
context,
timelineDataSourceFactory.create(savedStateHandle.get<String>("timelineId") != null),
circleFilterAccountDataManager
) {
......
......@@ -7,9 +7,9 @@ import org.futo.circles.core.model.LoadingData
import org.futo.circles.core.view.LoadingView
import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker
object UploadMediaProgressHelper {
object MediaProgressHelper {
fun getListener(loadingView: LoadingView): ContentUploadStateTracker.UpdateListener =
fun getUploadListener(loadingView: LoadingView): ContentUploadStateTracker.UpdateListener =
object : ContentUploadStateTracker.UpdateListener {
override fun onUpdate(state: ContentUploadStateTracker.State) {
when (state) {
......@@ -54,6 +54,5 @@ object UploadMediaProgressHelper {
else -> loadingView.gone()
}
}
}
}
\ No newline at end of file
package org.futo.circles.feature.timeline.list
import org.futo.circles.feature.timeline.list.holder.VideoPostViewHolder
interface OnVideoPlayBackStateListener {
fun onVideoPlaybackStateChanged(holder: VideoPostViewHolder, isPlaying: Boolean)
}
\ No newline at end of file
package org.futo.circles.feature.timeline.list
import org.futo.circles.core.model.PostContent
interface PostOptionsListener {
fun onShowMenuClicked(roomId: String, eventId: String)
fun onUserClicked(userId: String)
fun onShare(content: PostContent)
fun onReply(roomId: String, eventId: String)
fun onShowPreview(roomId: String, eventId: String)
fun onShowEmoji(roomId: String, eventId: String, onAddEmoji: (String) -> Unit)
fun onEmojiChipClicked(roomId: String, eventId: String, emoji: String, isUnSend: Boolean)
fun onPollOptionSelected(roomId: String, eventId: String, optionId: String)
}
\ No newline at end of file
package org.futo.circles.feature.timeline.list
import android.annotation.SuppressLint
import android.view.ViewGroup
import androidx.media3.exoplayer.ExoPlayer
import org.futo.circles.core.base.list.BaseRvAdapter
import org.futo.circles.core.feature.timeline.data_source.BaseTimelineDataSource
import org.futo.circles.core.model.Post
import org.futo.circles.core.model.PostContentType
import org.futo.circles.core.provider.MatrixSessionProvider
import org.futo.circles.feature.timeline.list.holder.ImagePostViewHolder
import org.futo.circles.feature.timeline.list.holder.MediaViewHolder
import org.futo.circles.feature.timeline.list.holder.PollPostViewHolder
import org.futo.circles.feature.timeline.list.holder.PostViewHolder
import org.futo.circles.feature.timeline.list.holder.TextPostViewHolder
import org.futo.circles.feature.timeline.list.holder.VideoPostViewHolder
import org.futo.circles.model.PostItemPayload
import org.futo.circles.view.PostOptionsListener
class TimelineAdapter(
private var userPowerLevel: Int,
private val postOptionsListener: PostOptionsListener,
private val isThread: Boolean,
private val videoPlayer: ExoPlayer,
private val onLoadMore: () -> Unit
) : BaseRvAdapter<Post, PostViewHolder>(PayloadIdEntityCallback { old, new ->
PostItemPayload(
......@@ -22,13 +28,12 @@ class TimelineAdapter(
reactions = new.reactionsData,
needToUpdateFullItem = new.content != old.content || new.postInfo != old.postInfo
)
}) {
}), OnVideoPlayBackStateListener {
@SuppressLint("NotifyDataSetChanged")
fun updateUserPowerLevel(level: Int) {
userPowerLevel = level
notifyDataSetChanged()
}
private var currentPlayingVideoHolder: VideoPostViewHolder? = null
private val uploadMediaTracker =
MatrixSessionProvider.getSessionOrThrow().contentUploadProgressTracker()
override fun getItemId(position: Int): Long = getItem(position).id.hashCode().toLong()
......@@ -36,16 +41,32 @@ class TimelineAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PostViewHolder {
return when (PostContentType.entries[viewType]) {
PostContentType.POLL_CONTENT -> PollPostViewHolder(
PostContentType.TEXT_CONTENT -> TextPostViewHolder(
parent, postOptionsListener, isThread
)
else -> TextMediaPostViewHolder(parent, postOptionsListener, isThread)
PostContentType.IMAGE_CONTENT -> ImagePostViewHolder(
parent, postOptionsListener, uploadMediaTracker, isThread
)
PostContentType.VIDEO_CONTENT -> VideoPostViewHolder(
parent,
postOptionsListener,
isThread,
uploadMediaTracker,
videoPlayer,
this
)
PostContentType.POLL_CONTENT -> PollPostViewHolder(
parent, postOptionsListener, isThread
)
}
}
override fun onBindViewHolder(holder: PostViewHolder, position: Int) {
holder.bind(getItem(position), userPowerLevel)
holder.bind(getItem(position))
if (position >= itemCount - BaseTimelineDataSource.LOAD_MORE_THRESHOLD) onLoadMore()
}
......@@ -59,7 +80,7 @@ class TimelineAdapter(
} else {
payloads.forEach {
val payload = (it as? PostItemPayload) ?: return@forEach
if (payload.needToUpdateFullItem) holder.bind(getItem(position), userPowerLevel)
if (payload.needToUpdateFullItem) holder.bind(getItem(position))
else holder.bindPayload(payload)
}
}
......@@ -67,7 +88,19 @@ class TimelineAdapter(
override fun onViewDetachedFromWindow(holder: PostViewHolder) {
super.onViewDetachedFromWindow(holder)
(holder as? UploadMediaViewHolder)?.uploadMediaTracker?.unTrack()
(holder as? MediaViewHolder)?.unTrackMediaLoading()
if (holder == currentPlayingVideoHolder) stopVideoPlayback()
}
override fun onVideoPlaybackStateChanged(holder: VideoPostViewHolder, isPlaying: Boolean) {
currentPlayingVideoHolder = if (isPlaying) {
stopVideoPlayback(false)
holder
} else null
}
fun stopVideoPlayback(shouldNotify: Boolean = true) {
currentPlayingVideoHolder?.stopVideo(shouldNotify)
}
}
\ No newline at end of file
package org.futo.circles.feature.timeline.list
import org.futo.circles.core.provider.MatrixSessionProvider
import org.futo.circles.core.view.LoadingView
import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker
interface UploadMediaViewHolder {
val uploadMediaTracker: UploadMediaTracker
}
class UploadMediaTracker {
private val tracker: ContentUploadStateTracker? =
MatrixSessionProvider.currentSession?.contentUploadProgressTracker()
private var postId: String? = null
private var listener: ContentUploadStateTracker.UpdateListener? = null
fun track(id: String, loadingView: LoadingView) {
postId = id
tracker?.track(id,
UploadMediaProgressHelper.getListener(loadingView).also { listener = it }
)
}
fun unTrack() {
val id = postId ?: return
val callback = listener ?: return
tracker?.untrack(id, callback)
}
}
\ No newline at end of file
package org.futo.circles.feature.timeline.list.holder
import android.view.ViewGroup
import org.futo.circles.core.base.list.ViewBindingHolder
import org.futo.circles.core.extensions.gone
import org.futo.circles.core.model.MediaContent
import org.futo.circles.core.model.Post
import org.futo.circles.databinding.ViewImagePostBinding
import org.futo.circles.feature.timeline.list.MediaProgressHelper
import org.futo.circles.feature.timeline.list.PostOptionsListener
import org.futo.circles.view.PostFooterView
import org.futo.circles.view.PostHeaderView
import org.futo.circles.view.PostStatusView
import org.futo.circles.view.ReadMoreTextView
import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker
class ImagePostViewHolder(
parent: ViewGroup,
postOptionsListener: PostOptionsListener,
private val uploadMediaTracker: ContentUploadStateTracker,
isThread: Boolean
) : PostViewHolder(inflate(parent, ViewImagePostBinding::inflate), postOptionsListener, isThread),
MediaViewHolder {
private companion object : ViewBindingHolder
private val binding = baseBinding as ViewImagePostBinding
override val postLayout: ViewGroup
get() = binding.lCard
override val postHeader: PostHeaderView
get() = binding.postHeader
override val postFooter: PostFooterView
get() = binding.postFooter
override val postStatus: PostStatusView
get() = binding.vPostStatus
override val readMoreTextView: ReadMoreTextView
get() = binding.tvTextContent
private val uploadListener: ContentUploadStateTracker.UpdateListener =
MediaProgressHelper.getUploadListener(binding.vLoadingView)
init {
setListeners()
binding.ivMediaContent.apply {
setOnClickListener {
post?.let { optionsListener.onShowPreview(it.postInfo.roomId, it.id) }
}
setOnLongClickListener {
postHeader.showMenu()
true
}
}
}
override fun bind(post: Post) {
super.bind(post)
with(binding) {
vLoadingView.gone()
val content = (post.content as? MediaContent) ?: return
bindMediaCaption(content, tvTextContent)
bindMediaCover(content, ivMediaContent)
uploadMediaTracker.track(post.id, uploadListener)
}
}
override fun unTrackMediaLoading() {
val key = post?.id ?: return
uploadMediaTracker.untrack(key, uploadListener)
}
}
\ No newline at end of file
package org.futo.circles.feature.timeline.list.holder
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.updateLayoutParams
import org.futo.circles.core.extensions.loadEncryptedThumbOrFullIntoWithAspect
import org.futo.circles.core.extensions.setIsVisible
import org.futo.circles.core.model.MediaContent
interface MediaViewHolder {
fun unTrackMediaLoading()
fun bindMediaCaption(content: MediaContent, textView: TextView) {
textView.apply {
val caption = content.captionSpanned
setIsVisible(caption != null)
caption?.let { setText(it, TextView.BufferType.SPANNABLE) }
}
}
fun bindMediaCover(content: MediaContent, image: ImageView) {
image.post {
val size = content.calculateThumbnailSize(image.width)
image.updateLayoutParams {
width = size.width
height = size.height
}
}
content.loadEncryptedThumbOrFullIntoWithAspect(image)
}
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment