diff --git a/app/src/main/java/com/futo/circles/base/BaseRecycleViewDecoration.kt b/app/src/main/java/com/futo/circles/base/BaseRecycleViewDecoration.kt new file mode 100644 index 0000000000000000000000000000000000000000..6bec72b6f483ed325be41be998fe1640404db441 --- /dev/null +++ b/app/src/main/java/com/futo/circles/base/BaseRecycleViewDecoration.kt @@ -0,0 +1,89 @@ +package com.futo.circles.base + +import android.graphics.Canvas +import android.graphics.Rect +import android.view.View +import androidx.recyclerview.widget.RecyclerView + + +abstract class BaseRvDecoration<in T : RecyclerView.ViewHolder> : RecyclerView.ItemDecoration() { + + override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) = + if (shouldDraw()) draw(c, parent, state) else super.onDraw(c, parent, state) + + override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) = + if (shouldDrawOver()) drawOver(c, parent, state) else super.onDrawOver(c, parent, state) + + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + @Suppress("UNCHECKED_CAST") + val holder = parent.getChildViewHolder(view) as? T + + holder?.takeIf(::shouldOffsetHolder)?.let { offsetHolder(outRect, holder, parent, state) } + ?: super.getItemOffsets(outRect, view, parent, state) + } + + open fun shouldDraw(): Boolean = false + open fun shouldDrawOver(): Boolean = false + open fun shouldOffsetHolder(holder: T): Boolean = false + + open fun draw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { + + } + + open fun drawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { + + } + + open fun offsetHolder( + outRect: Rect, + holder: T, + parent: RecyclerView, + state: RecyclerView.State + ) { + + } + + class OffsetDecoration<in T : RecyclerView.ViewHolder>( + private val topOffset: Int = 0, + private val bottomOffset: Int = 0, + private val leftOffset: Int = 0, + private val rightOffset: Int = 0 + ) : BaseRvDecoration<T>() { + override fun shouldOffsetHolder(holder: T): Boolean = true + + constructor(verticalOffset: Int, horizontalOffset: Int) : this( + topOffset = verticalOffset, + bottomOffset = verticalOffset, + leftOffset = horizontalOffset, + rightOffset = horizontalOffset + ) + + constructor(offset: Int) : this( + topOffset = offset, + bottomOffset = offset, + leftOffset = offset, + rightOffset = offset + ) + + override fun offsetHolder( + outRect: Rect, + holder: T, + parent: RecyclerView, + state: RecyclerView.State + ) { + outRect.left = leftOffset + outRect.right = rightOffset + outRect.top = topOffset + outRect.bottom = bottomOffset + } + } + +} + + + diff --git a/app/src/main/java/com/futo/circles/base/BaseRecyclerView.kt b/app/src/main/java/com/futo/circles/base/BaseRecyclerView.kt index 09f7511aae85d929466b873633c1deee73b1c220..c89df71651d17e1d4eaf50b8a8c54df8f23b44aa 100644 --- a/app/src/main/java/com/futo/circles/base/BaseRecyclerView.kt +++ b/app/src/main/java/com/futo/circles/base/BaseRecyclerView.kt @@ -3,26 +3,29 @@ package com.futo.circles.base import android.annotation.SuppressLint import android.content.Context import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding -abstract class BaseRecyclerViewHolder<T, VB : ViewBinding> : RecyclerView.ViewHolder { - protected val binding: VB +interface ViewBindingHolder{ - protected constructor( - parent: ViewGroup, - inflate: (LayoutInflater, ViewGroup?, Boolean) -> VB - ) : this(inflate.invoke(LayoutInflater.from(parent.context), parent, false)) + val baseBinding: ViewBinding get() = viewBinding - private constructor(viewBinding: VB) : super(viewBinding.root) { - this.binding = viewBinding + fun inflate( + parent: ViewGroup, + inflate: (LayoutInflater, ViewGroup?, Boolean) -> ViewBinding + ): View { + viewBinding = inflate.invoke(LayoutInflater.from(parent.context), parent, false) + return viewBinding.root } - abstract fun bind(data: T) + private companion object { + private lateinit var viewBinding: ViewBinding + } } val RecyclerView.ViewHolder.context: Context get() = this.itemView.context @@ -31,16 +34,24 @@ abstract class BaseRvAdapter<T, VH : RecyclerView.ViewHolder>( itemCallback: DiffUtil.ItemCallback<T> ) : ListAdapter<T, VH>(itemCallback) { - @Suppress("UNCHECKED_CAST") - protected fun <D : T> getItemAs(position: Int): D = getItem(position) as D - companion object { @Suppress("FunctionName") @SuppressLint("DiffUtilEquals") - fun <T> DefaultDiffUtilCallback() = object : DiffUtil.ItemCallback<T>() { - override fun areItemsTheSame(oldItem: T, newItem: T): Boolean = oldItem == newItem + fun <T : IdEntity<*>> DefaultIdEntityCallback() = object : DiffUtil.ItemCallback<T>() { + override fun areItemsTheSame(oldItem: T, newItem: T): Boolean = oldItem.id == newItem.id override fun areContentsTheSame(oldItem: T, newItem: T): Boolean = oldItem == newItem } + + @Suppress("FunctionName") + @SuppressLint("DiffUtilEquals") + fun <T : IdEntity<*>, C> PayloadIdEntityCallback( + payload: (old: T, new: T) -> C? + ) = object : DiffUtil.ItemCallback<T>() { + override fun areItemsTheSame(oldItem: T, newItem: T): Boolean = oldItem.id == newItem.id + override fun areContentsTheSame(oldItem: T, newItem: T): Boolean = oldItem == newItem + + override fun getChangePayload(oldItem: T, newItem: T) = payload(oldItem, newItem) + } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/base/IdEntity.kt b/app/src/main/java/com/futo/circles/base/IdEntity.kt new file mode 100644 index 0000000000000000000000000000000000000000..7004f2f979a7b29354bd2644596a5cfbf128bdb5 --- /dev/null +++ b/app/src/main/java/com/futo/circles/base/IdEntity.kt @@ -0,0 +1,5 @@ +package com.futo.circles.base + +interface IdEntity<out IdClass> { + val id: IdClass +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/di/DataSourceModule.kt b/app/src/main/java/com/futo/circles/di/DataSourceModule.kt index a2e49b52fe9d9373114bff989daf4de2620a50c7..e570f965d3adbbb95b33f9c975b4e57122a467fa 100644 --- a/app/src/main/java/com/futo/circles/di/DataSourceModule.kt +++ b/app/src/main/java/com/futo/circles/di/DataSourceModule.kt @@ -1,8 +1,14 @@ package com.futo.circles.di +import com.futo.circles.ui.groups.timeline.data_source.GroupTimelineBuilder +import com.futo.circles.ui.groups.timeline.data_source.GroupTimelineDatasource import com.futo.circles.ui.log_in.data_source.LoginDataSource import org.koin.dsl.module val dataSourceModule = module { - factory { LoginDataSource(get()) } + factory { LoginDataSource(get(), get()) } + + factory { (roomId: String) -> GroupTimelineDatasource(roomId, get(), get()) } + + factory { GroupTimelineBuilder() } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/di/UiModule.kt b/app/src/main/java/com/futo/circles/di/UiModule.kt index e57fbe50965985dc0e9c6251a4f7d49d5524ef7e..8b5d91860061f3954b686123e4a240442abebf20 100644 --- a/app/src/main/java/com/futo/circles/di/UiModule.kt +++ b/app/src/main/java/com/futo/circles/di/UiModule.kt @@ -4,10 +4,11 @@ import com.futo.circles.ui.groups.GroupsViewModel import com.futo.circles.ui.groups.timeline.GroupTimelineViewModel import com.futo.circles.ui.log_in.LogInViewModel import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.core.parameter.parametersOf import org.koin.dsl.module val uiModule = module { viewModel { LogInViewModel(get()) } viewModel { GroupsViewModel(get()) } - viewModel { (roomId: String) -> GroupTimelineViewModel(roomId, get()) } + viewModel { (roomId: String) -> GroupTimelineViewModel(get { parametersOf(roomId) }) } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/extensions/ContextExtensions.kt b/app/src/main/java/com/futo/circles/extensions/ContextExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..53b4b0c7493b083d4dadef65f34ff2de3b4de7d3 --- /dev/null +++ b/app/src/main/java/com/futo/circles/extensions/ContextExtensions.kt @@ -0,0 +1,6 @@ +package com.futo.circles.extensions + +import android.content.Context +import android.support.annotation.DimenRes + +fun Context.dimen(@DimenRes resource: Int): Int = resources.getDimensionPixelSize(resource) \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/extensions/ImageViewExtensions.kt b/app/src/main/java/com/futo/circles/extensions/ImageViewExtensions.kt index fb1f33b9e18e2c695db1a49b5e51d13698a556b0..c2b3ccf7b4a60130febb62c464ee2201e2f77230 100644 --- a/app/src/main/java/com/futo/circles/extensions/ImageViewExtensions.kt +++ b/app/src/main/java/com/futo/circles/extensions/ImageViewExtensions.kt @@ -1,6 +1,7 @@ package com.futo.circles.extensions import android.widget.ImageView +import com.futo.circles.R import com.squareup.picasso.Picasso import org.matrix.android.sdk.api.session.content.ContentUrlResolver @@ -17,4 +18,8 @@ fun ImageView.loadMatrixThumbnail( ContentUrlResolver.ThumbnailMethod.SCALE ) Picasso.get().load(resolvedUrl).into(this) +} + +fun ImageView.setIsEncryptedIcon(isEncrypted: Boolean) { + setImageResource(if (isEncrypted) R.drawable.ic_lock else R.drawable.ic_lock_open) } \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/extensions/MatrixRoomExtensions.kt b/app/src/main/java/com/futo/circles/extensions/MatrixRoomExtensions.kt index d0797744aa234b43d028dbcc3b54e62046f10ced..f5d674f0af5dbb9e656ad6f5c6bc9c4566c34d86 100644 --- a/app/src/main/java/com/futo/circles/extensions/MatrixRoomExtensions.kt +++ b/app/src/main/java/com/futo/circles/extensions/MatrixRoomExtensions.kt @@ -1,9 +1,11 @@ package com.futo.circles.extensions +import com.futo.circles.mapping.toGroupListItem import org.matrix.android.sdk.api.session.room.model.RoomSummary -fun List<RoomSummary>.containsTag(tagName: String) = filter { room -> - room.tags.firstOrNull { tag -> tag.name.contains(tagName) }?.let { true } ?: false +fun List<RoomSummary>.toGroupsList(tagName: String) = mapNotNull { room -> + val isGroup = room.tags.firstOrNull { tag -> tag.name.contains(tagName) }?.let { true } ?: false + if (isGroup) room.toGroupListItem() else null } fun RoomSummary.nameOrId() = displayName.takeIf { it.isNotEmpty() } ?: roomId \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/extensions/RecyclerViewExtensions.kt b/app/src/main/java/com/futo/circles/extensions/RecyclerViewExtensions.kt index cb01b978ce9b9c6f0a98d5574bb157c557f7b007..7eddf12ac4c7dff44dbffeccfa7949a9389dac1f 100644 --- a/app/src/main/java/com/futo/circles/extensions/RecyclerViewExtensions.kt +++ b/app/src/main/java/com/futo/circles/extensions/RecyclerViewExtensions.kt @@ -2,7 +2,20 @@ package com.futo.circles.extensions import android.view.View +import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.floatingactionbutton.FloatingActionButton fun RecyclerView.ViewHolder.onClick(view: View, perform: (adapterPosition: Int) -> Unit) = - view.setOnClickListener { bindingAdapterPosition.takeIf { it != -1 }?.let(perform) } \ No newline at end of file + view.setOnClickListener { bindingAdapterPosition.takeIf { it != -1 }?.let(perform) } + +// Kotlin +fun RecyclerView.bindToFab(fab:FloatingActionButton) { + addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + if (dy > 0 && fab.isVisible) fab.hide() + else if (dy < 0 && fab.visibility != View.VISIBLE) fab.show() + } + }) +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/mapping/RoomSummaryMapping.kt b/app/src/main/java/com/futo/circles/mapping/RoomSummaryMapping.kt new file mode 100644 index 0000000000000000000000000000000000000000..b81d887cfa4ea6111cfbfbffca7c1014db50267f --- /dev/null +++ b/app/src/main/java/com/futo/circles/mapping/RoomSummaryMapping.kt @@ -0,0 +1,15 @@ +package com.futo.circles.mapping + +import com.futo.circles.extensions.nameOrId +import com.futo.circles.model.GroupListItem +import org.matrix.android.sdk.api.session.room.model.RoomSummary + +fun RoomSummary.toGroupListItem() = GroupListItem( + id = roomId, + title = nameOrId(), + topic = topic, + membersCount = joinedMembersCount ?: 0, + timestamp = latestPreviewableEvent?.root?.originServerTs ?: System.currentTimeMillis(), + isEncrypted = isEncrypted, + avatarUrl = avatarUrl +) \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/mapping/TimelineEventMapping.kt b/app/src/main/java/com/futo/circles/mapping/TimelineEventMapping.kt new file mode 100644 index 0000000000000000000000000000000000000000..e6474a0339031552ccf869ba18ddc3ece4f04779 --- /dev/null +++ b/app/src/main/java/com/futo/circles/mapping/TimelineEventMapping.kt @@ -0,0 +1,51 @@ +package com.futo.circles.mapping + +import com.futo.circles.model.* +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.getRelationContent +import org.matrix.android.sdk.api.session.room.timeline.isReply + + +fun TimelineEvent.toPost( + postContentType: PostContentType, + isRepliesVisible: Boolean = false +): Post = + if (isReply()) toReplyPost(postContentType) else toRootPost(postContentType, isRepliesVisible) + +private fun TimelineEvent.toPostInfo(): PostInfo = PostInfo( + id = eventId, + isEncrypted = isEncrypted(), + timestamp = root.originServerTs ?: System.currentTimeMillis(), + sender = senderInfo +) + +private fun TimelineEvent.toRootPost(postContentType: PostContentType, isRepliesVisible: Boolean) = + RootPost( + postInfo = toPostInfo(), + content = toPostContent(postContentType), + isRepliesVisible = isRepliesVisible, + ) + +private fun TimelineEvent.toReplyPost(postContentType: PostContentType) = ReplyPost( + postInfo = toPostInfo(), + content = toPostContent(postContentType), + replyToId = getRelationContent()?.inReplyTo?.eventId ?: "", +) + +private fun TimelineEvent.toPostContent(postContentType: PostContentType): PostContent = + when (postContentType) { + PostContentType.TEXT_CONTENT -> toTextContent() + PostContentType.IMAGE_CONTENT -> toImageContent() + } + +private fun TimelineEvent.toTextContent(): TextContent = TextContent( + message = root.getClearContent().toModel<MessageTextContent>()?.body ?: "" +) + +private fun TimelineEvent.toImageContent(): ImageContent = ImageContent( + url = root.getClearContent() + .toModel<MessageImageContent>()?.info?.thumbnailFile?.url ?: "" +) diff --git a/app/src/main/java/com/futo/circles/model/GroupListItem.kt b/app/src/main/java/com/futo/circles/model/GroupListItem.kt new file mode 100644 index 0000000000000000000000000000000000000000..9308a0fdf39e9eca55a88725fde439eb2c8018b4 --- /dev/null +++ b/app/src/main/java/com/futo/circles/model/GroupListItem.kt @@ -0,0 +1,13 @@ +package com.futo.circles.model + +import com.futo.circles.base.IdEntity + +data class GroupListItem( + override val id: String, + val title: String, + val topic: String, + val isEncrypted: Boolean, + val avatarUrl: String, + val membersCount: Int, + val timestamp: Long +) : IdEntity<String> \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/model/GroupListItemPayload.kt b/app/src/main/java/com/futo/circles/model/GroupListItemPayload.kt new file mode 100644 index 0000000000000000000000000000000000000000..edb4c6214700f46853aab959855ca9bbf12a000c --- /dev/null +++ b/app/src/main/java/com/futo/circles/model/GroupListItemPayload.kt @@ -0,0 +1,6 @@ +package com.futo.circles.model + +data class GroupListItemPayload( + val membersCount: Int, + val timestamp: Long +) diff --git a/app/src/main/java/com/futo/circles/model/Post.kt b/app/src/main/java/com/futo/circles/model/Post.kt new file mode 100644 index 0000000000000000000000000000000000000000..bc53ea76428b0f6ba82bd9a4a765b66bb22d7df0 --- /dev/null +++ b/app/src/main/java/com/futo/circles/model/Post.kt @@ -0,0 +1,26 @@ +package com.futo.circles.model + +import com.futo.circles.base.IdEntity + +sealed class Post( + open val postInfo: PostInfo, + open val content: PostContent +) : IdEntity<String> { + override val id: String get() = postInfo.id +} + +data class RootPost( + override val postInfo: PostInfo, + override val content: PostContent, + val replies: List<ReplyPost> = emptyList(), + val isRepliesVisible: Boolean = false +) : Post(postInfo, content) { + fun hasReplies(): Boolean = replies.isNotEmpty() + fun getRepliesCount(): Int = replies.size +} + +data class ReplyPost( + override val postInfo: PostInfo, + override val content: PostContent, + val replyToId: String +) : Post(postInfo, content) \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/model/PostContent.kt b/app/src/main/java/com/futo/circles/model/PostContent.kt new file mode 100644 index 0000000000000000000000000000000000000000..04b63f497342a597b694ebf1812e7991ee361de9 --- /dev/null +++ b/app/src/main/java/com/futo/circles/model/PostContent.kt @@ -0,0 +1,17 @@ +package com.futo.circles.model + +import org.matrix.android.sdk.api.session.room.model.message.MessageType + +enum class PostContentType(val typeKey: String) { + TEXT_CONTENT(MessageType.MSGTYPE_TEXT), IMAGE_CONTENT(MessageType.MSGTYPE_IMAGE) +} + +sealed class PostContent(val type: PostContentType) + +data class TextContent( + val message: String +) : PostContent(PostContentType.TEXT_CONTENT) + +data class ImageContent( + val url: String +) : PostContent(PostContentType.IMAGE_CONTENT) \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/model/PostInfo.kt b/app/src/main/java/com/futo/circles/model/PostInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..cb2661abd1e9c7e59ce78414ce70d7aabafe7a51 --- /dev/null +++ b/app/src/main/java/com/futo/circles/model/PostInfo.kt @@ -0,0 +1,10 @@ +package com.futo.circles.model + +import org.matrix.android.sdk.api.session.room.sender.SenderInfo + +data class PostInfo( + val id: String, + val sender: SenderInfo, + val isEncrypted: Boolean, + val timestamp: Long +) \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/model/PostItemPayload.kt b/app/src/main/java/com/futo/circles/model/PostItemPayload.kt new file mode 100644 index 0000000000000000000000000000000000000000..613d557000023f3bcc4507dfc19a4b40525b546b --- /dev/null +++ b/app/src/main/java/com/futo/circles/model/PostItemPayload.kt @@ -0,0 +1,7 @@ +package com.futo.circles.model + +class PostItemPayload( + val repliesCount: Int, + val isRepliesVisible: Boolean, + val hasReplies: Boolean +) \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/provider/MatrixProvider.kt b/app/src/main/java/com/futo/circles/provider/MatrixProvider.kt deleted file mode 100644 index 6d29dd1fce264241531dffbf2d4c9749215dc1b1..0000000000000000000000000000000000000000 --- a/app/src/main/java/com/futo/circles/provider/MatrixProvider.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.futo.circles.provider - -import org.matrix.android.sdk.api.Matrix - -object MatrixProvider { - lateinit var matrix: Matrix - private set - - fun saveMatrixInstance(matrixInstance: Matrix) { - matrix = matrixInstance - } -} \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/provider/MatrixSessionProvider.kt b/app/src/main/java/com/futo/circles/provider/MatrixSessionProvider.kt index 6671e623ca14ea8d7a96d929e542e322df6bd776..6344e1e54d7283f630324959debc9dfed2723589 100644 --- a/app/src/main/java/com/futo/circles/provider/MatrixSessionProvider.kt +++ b/app/src/main/java/com/futo/circles/provider/MatrixSessionProvider.kt @@ -16,11 +16,9 @@ class MatrixSessionProvider(private val context: Context) { roomDisplayNameFallbackProvider = RoomDisplayNameFallbackProviderImpl() ) ) - val matrixInstance = - Matrix.getInstance(context).also { MatrixProvider.saveMatrixInstance(it) } val lastSession = - matrixInstance.authenticationService().getLastAuthenticatedSession() + Matrix.getInstance(context).authenticationService().getLastAuthenticatedSession() lastSession?.let { startSession(it) } } diff --git a/app/src/main/java/com/futo/circles/ui/groups/GroupsFragment.kt b/app/src/main/java/com/futo/circles/ui/groups/GroupsFragment.kt index 9d60a949ad5efde4883f37264008395db9b077c7..36620e2708edfe93b2fd835435cbee0d91bd508d 100644 --- a/app/src/main/java/com/futo/circles/ui/groups/GroupsFragment.kt +++ b/app/src/main/java/com/futo/circles/ui/groups/GroupsFragment.kt @@ -9,11 +9,9 @@ import by.kirich1409.viewbindingdelegate.viewBinding import com.futo.circles.R import com.futo.circles.databinding.GroupsFragmentBinding import com.futo.circles.extensions.observeData -import com.futo.circles.provider.MatrixSessionProvider +import com.futo.circles.model.GroupListItem import com.futo.circles.ui.groups.list.GroupsListAdapter -import org.koin.android.ext.android.get import org.koin.androidx.viewmodel.ext.android.viewModel -import org.matrix.android.sdk.api.session.room.model.RoomSummary class GroupsFragment : Fragment(R.layout.groups_fragment) { @@ -22,7 +20,7 @@ class GroupsFragment : Fragment(R.layout.groups_fragment) { private val binding by viewBinding(GroupsFragmentBinding::bind) private val listAdapter by lazy { GroupsListAdapter( - get<MatrixSessionProvider>().currentSession?.contentUrlResolver(), + viewModel.getContentResolver(), ::onGroupListItemClicked ) } @@ -38,15 +36,13 @@ class GroupsFragment : Fragment(R.layout.groups_fragment) { viewModel.groupsLiveData?.observeData(this, ::setGroupsList) } - private fun setGroupsList(list: List<RoomSummary>) { + private fun setGroupsList(list: List<GroupListItem>) { listAdapter.submitList(list) } - private fun onGroupListItemClicked(room: RoomSummary) { + private fun onGroupListItemClicked(room: GroupListItem) { findNavController().navigate( - GroupsFragmentDirections.actionGroupsFragment2ToGroupTimelineFragment( - room.roomId - ) + GroupsFragmentDirections.actionGroupsFragment2ToGroupTimelineFragment(room.id) ) } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/ui/groups/GroupsViewModel.kt b/app/src/main/java/com/futo/circles/ui/groups/GroupsViewModel.kt index 0a0776ee6c046d8dec9001cd62a7c659cad2d325..a0b313431dd17234c33a0208f6e2bfe28bec6d68 100644 --- a/app/src/main/java/com/futo/circles/ui/groups/GroupsViewModel.kt +++ b/app/src/main/java/com/futo/circles/ui/groups/GroupsViewModel.kt @@ -2,17 +2,19 @@ package com.futo.circles.ui.groups import androidx.lifecycle.ViewModel import androidx.lifecycle.map -import com.futo.circles.extensions.containsTag +import com.futo.circles.extensions.toGroupsList import com.futo.circles.provider.MatrixSessionProvider import com.futo.circles.utils.GROUP_TAG import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams class GroupsViewModel( - matrixSessionProvider: MatrixSessionProvider + private val matrixSessionProvider: MatrixSessionProvider ) : ViewModel() { val groupsLiveData = matrixSessionProvider.currentSession?.getRoomSummariesLive(roomSummaryQueryParams()) - ?.map { list -> list.containsTag(GROUP_TAG) } + ?.map { list -> list.toGroupsList(GROUP_TAG) } + + fun getContentResolver() = matrixSessionProvider.currentSession?.contentUrlResolver() } diff --git a/app/src/main/java/com/futo/circles/ui/groups/list/GroupViewHolder.kt b/app/src/main/java/com/futo/circles/ui/groups/list/GroupViewHolder.kt index 57bd8dac82e94de52c5ebd93ac9b9d9fe796fbaf..f27d1fd318dfc53bbf7270736e9cb595acbe3888 100644 --- a/app/src/main/java/com/futo/circles/ui/groups/list/GroupViewHolder.kt +++ b/app/src/main/java/com/futo/circles/ui/groups/list/GroupViewHolder.kt @@ -2,57 +2,68 @@ package com.futo.circles.ui.groups.list import android.text.format.DateUtils import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView import com.futo.circles.R -import com.futo.circles.base.BaseRecyclerViewHolder +import com.futo.circles.base.ViewBindingHolder import com.futo.circles.base.context import com.futo.circles.databinding.GroupListItemBinding import com.futo.circles.extensions.loadMatrixThumbnail -import com.futo.circles.extensions.nameOrId import com.futo.circles.extensions.onClick +import com.futo.circles.extensions.setIsEncryptedIcon +import com.futo.circles.model.GroupListItem +import com.futo.circles.model.GroupListItemPayload import org.matrix.android.sdk.api.session.content.ContentUrlResolver -import org.matrix.android.sdk.api.session.room.model.RoomSummary class GroupViewHolder( parent: ViewGroup, private val urlResolver: ContentUrlResolver?, onGroupClicked: (Int) -> Unit -) : BaseRecyclerViewHolder<RoomSummary, GroupListItemBinding>( - parent, - GroupListItemBinding::inflate -) { +) : RecyclerView.ViewHolder(inflate(parent, GroupListItemBinding::inflate)) { + + private companion object : ViewBindingHolder + + private val binding = baseBinding as GroupListItemBinding init { onClick(itemView) { position -> onGroupClicked(position) } } - override fun bind(data: RoomSummary) { + fun bind(data: GroupListItem) { with(binding) { ivGroup.loadMatrixThumbnail(data.avatarUrl, urlResolver) - ivLock.setImageResource(if (data.isEncrypted) R.drawable.ic_lock else R.drawable.ic_lock_open) + ivLock.setIsEncryptedIcon(data.isEncrypted) - tvGroupTitle.text = data.nameOrId() - - val membersCount = data.joinedMembersCount ?: 0 - tvMembers.text = context.resources.getQuantityString( - R.plurals.member_plurals, - membersCount, membersCount - ) + tvGroupTitle.text = data.title + + setMembersCount(data.membersCount) tvTopic.text = context.getString( R.string.topic_formatter, data.topic.takeIf { it.isNotEmpty() } ?: context.getString(R.string.none) ) - data.latestPreviewableEvent?.root?.originServerTs?.let { time -> - tvUpdateTime.text = context.getString( - R.string.last_updated_formatter, DateUtils.getRelativeTimeSpanString( - time, - System.currentTimeMillis(), - DateUtils.HOUR_IN_MILLIS - ) - ) - } + setUpdateTime(data.timestamp) } } + + fun bindPayload(data: GroupListItemPayload) { + setMembersCount(data.membersCount) + setUpdateTime(data.timestamp) + } + + private fun setMembersCount(membersCount: Int) { + binding.tvMembers.text = context.resources.getQuantityString( + R.plurals.member_plurals, + membersCount, membersCount + ) + } + + private fun setUpdateTime(timestamp: Long) { + binding.tvUpdateTime.text = context.getString( + R.string.last_updated_formatter, DateUtils.getRelativeTimeSpanString( + timestamp, System.currentTimeMillis(), DateUtils.SECOND_IN_MILLIS + ) + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/ui/groups/list/GroupsListAdapter.kt b/app/src/main/java/com/futo/circles/ui/groups/list/GroupsListAdapter.kt index 6395ce4e8a110071824f0d29087d0a4bd387efc4..bd02c75631ce659276e24293654f75f2970227f8 100644 --- a/app/src/main/java/com/futo/circles/ui/groups/list/GroupsListAdapter.kt +++ b/app/src/main/java/com/futo/circles/ui/groups/list/GroupsListAdapter.kt @@ -2,25 +2,44 @@ package com.futo.circles.ui.groups.list import android.view.ViewGroup import com.futo.circles.base.BaseRvAdapter +import com.futo.circles.model.GroupListItem +import com.futo.circles.model.GroupListItemPayload import org.matrix.android.sdk.api.session.content.ContentUrlResolver -import org.matrix.android.sdk.api.session.room.model.RoomSummary class GroupsListAdapter( private val urlResolver: ContentUrlResolver?, - private val onGroupClicked: (RoomSummary) -> Unit -) : BaseRvAdapter<RoomSummary, GroupViewHolder>(DefaultDiffUtilCallback()) { - + private val onGroupClicked: (GroupListItem) -> Unit +) : BaseRvAdapter<GroupListItem, GroupViewHolder>(PayloadIdEntityCallback { _, new -> + GroupListItemPayload( + membersCount = new.membersCount, + timestamp = new.timestamp + ) +}) { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int ): GroupViewHolder = GroupViewHolder( parent = parent, urlResolver = urlResolver, - onGroupClicked = { position -> getItem(position)?.let { onGroupClicked(it) } } + onGroupClicked = { position -> onGroupClicked(getItem(position)) } ) override fun onBindViewHolder(holder: GroupViewHolder, position: Int) { - getItem(position)?.let { holder.bind(it) } + holder.bind(getItem(position)) + } + + override fun onBindViewHolder( + holder: GroupViewHolder, + position: Int, + payloads: MutableList<Any> + ) { + if (payloads.isNullOrEmpty()) { + super.onBindViewHolder(holder, position, payloads) + } else { + payloads.forEach { + (it as? GroupListItemPayload)?.let { payload -> holder.bindPayload(payload) } + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/ui/groups/timeline/GroupTimelineFragment.kt b/app/src/main/java/com/futo/circles/ui/groups/timeline/GroupTimelineFragment.kt index fa16b9618902bf788fef3f12268de069d135d201..b4092777cef57b97b76130c8350da88e1cf6a26d 100644 --- a/app/src/main/java/com/futo/circles/ui/groups/timeline/GroupTimelineFragment.kt +++ b/app/src/main/java/com/futo/circles/ui/groups/timeline/GroupTimelineFragment.kt @@ -6,21 +6,56 @@ import androidx.fragment.app.Fragment import androidx.navigation.fragment.navArgs import by.kirich1409.viewbindingdelegate.viewBinding import com.futo.circles.R +import com.futo.circles.base.BaseRvDecoration import com.futo.circles.databinding.GroupTimelineFragmentBinding +import com.futo.circles.extensions.bindToFab +import com.futo.circles.extensions.dimen import com.futo.circles.extensions.observeData import com.futo.circles.extensions.setToolbarTitle +import com.futo.circles.model.Post +import com.futo.circles.ui.groups.timeline.list.GroupPostViewHolder +import com.futo.circles.ui.groups.timeline.list.GroupTimelineAdapter +import com.futo.circles.ui.view.GroupPostListener import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf -class GroupTimelineFragment : Fragment(R.layout.group_timeline_fragment) { +class GroupTimelineFragment : Fragment(R.layout.group_timeline_fragment), GroupPostListener { private val args: GroupTimelineFragmentArgs by navArgs() private val viewModel by viewModel<GroupTimelineViewModel> { parametersOf(args.roomId) } private val binding by viewBinding(GroupTimelineFragmentBinding::bind) + private val listAdapter by lazy { + GroupTimelineAdapter(this, viewModel.urlResolver) { viewModel.loadMore() } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - viewModel.titleLiveData.observeData(this) { title -> setToolbarTitle(title) } + binding.tvGroupTimeline.apply { + adapter = listAdapter + addItemDecoration( + BaseRvDecoration.OffsetDecoration<GroupPostViewHolder>( + offset = context.dimen(R.dimen.group_post_item_offset) + ) + ) + bindToFab(binding.fbCreatePost) + } + setupObservers() + } + + private fun setupObservers() { + with(viewModel) { + titleLiveData.observeData(this@GroupTimelineFragment) { title -> setToolbarTitle(title) } + timelineEventsLiveData.observeData(this@GroupTimelineFragment, ::setTimelineList) + } + } + + private fun setTimelineList(list: List<Post>) { + listAdapter.submitList(list) + } + + override fun onShowRepliesClicked(eventId: String) { + viewModel.toggleRepliesVisibilityFor(eventId) } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/ui/groups/timeline/GroupTimelineViewModel.kt b/app/src/main/java/com/futo/circles/ui/groups/timeline/GroupTimelineViewModel.kt index de15ed4849a462090e25a6830c51ce95073c8e6e..76142252a308820e2a9169f60787441eccd7c217 100644 --- a/app/src/main/java/com/futo/circles/ui/groups/timeline/GroupTimelineViewModel.kt +++ b/app/src/main/java/com/futo/circles/ui/groups/timeline/GroupTimelineViewModel.kt @@ -2,16 +2,31 @@ package com.futo.circles.ui.groups.timeline import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import com.futo.circles.extensions.nameOrId -import com.futo.circles.provider.MatrixSessionProvider +import com.futo.circles.ui.groups.timeline.data_source.GroupTimelineDatasource class GroupTimelineViewModel( - private val roomId: String, - private val matrixSessionProvider: MatrixSessionProvider + private val dataSource: GroupTimelineDatasource ) : ViewModel() { - val titleLiveData = MutableLiveData(getRoom()?.roomSummary()?.nameOrId() ?: roomId) + val titleLiveData = MutableLiveData(dataSource.getGroupTitle()) + val timelineEventsLiveData = dataSource.timelineEventsLiveData + val urlResolver get() = dataSource.getUrlResolver() - private fun getRoom() = matrixSessionProvider.currentSession?.getRoom(roomId) + init { + dataSource.startTimeline() + } + + fun loadMore() { + dataSource.loadMore() + } + + fun toggleRepliesVisibilityFor(eventId: String) { + dataSource.toggleRepliesVisibility(eventId) + } + + override fun onCleared() { + dataSource.clearTimeline() + super.onCleared() + } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/ui/groups/timeline/data_source/GroupTimelineBuilder.kt b/app/src/main/java/com/futo/circles/ui/groups/timeline/data_source/GroupTimelineBuilder.kt new file mode 100644 index 0000000000000000000000000000000000000000..0a51eb62da5acc095a207aadd14e13c2b9675581 --- /dev/null +++ b/app/src/main/java/com/futo/circles/ui/groups/timeline/data_source/GroupTimelineBuilder.kt @@ -0,0 +1,105 @@ +package com.futo.circles.ui.groups.timeline.data_source + +import com.futo.circles.mapping.toPost +import com.futo.circles.model.Post +import com.futo.circles.model.PostContentType +import com.futo.circles.model.ReplyPost +import com.futo.circles.model.RootPost +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +class GroupTimelineBuilder { + + private val repliesVisibleEvents: MutableSet<String> = mutableSetOf() + + private var currentList: MutableList<Post> = mutableListOf() + + fun build(list: List<TimelineEvent>): List<Post> { + val messageTimelineEvents = getOnlyMessageTimelineEvents(list) + val groupMessages = transformToGroupPosts(messageTimelineEvents) + val messagesWithReplies = setupRootMessagesWithVisibleReplies(groupMessages) + return toFlatPostsList(messagesWithReplies).also { currentList = it } + } + + fun toggleRepliesVisibilityFor(eventId: String): List<Post> = + if (isRepliesVisibleFor(eventId)) removeRepliesFromCurrentListFor(eventId) + else addRepliesToCurrentListFor(eventId) + + private fun addRepliesToCurrentListFor(eventId: String): List<Post> { + val list: MutableList<Post> = mutableListOf() + repliesVisibleEvents.add(eventId) + currentList.forEach { post -> + if (post.id == eventId && post is RootPost) { + list.add(post.copy(isRepliesVisible = true)) + list.addAll(post.replies) + } else { + list.add(post) + } + } + return list.also { currentList = list } + } + + private fun removeRepliesFromCurrentListFor(eventId: String): List<Post> { + val list: MutableList<Post> = mutableListOf() + repliesVisibleEvents.remove(eventId) + val rootPostsList = getOnlyRootMessages(currentList) + rootPostsList.forEach { rootPost -> + if (rootPost.id == eventId) { + list.add(rootPost.copy(isRepliesVisible = false)) + } else { + list.add(rootPost) + if (rootPost.isRepliesVisible) list.addAll(rootPost.replies) + } + } + return list.also { currentList = list } + } + + private fun toFlatPostsList(messagesWithReplies: List<RootPost>): MutableList<Post> { + val list: MutableList<Post> = mutableListOf() + messagesWithReplies.forEach { message -> + list.add(message) + if (message.isRepliesVisible) list.addAll(message.replies) + } + return list + } + + private fun getOnlyMessageTimelineEvents(list: List<TimelineEvent>): List<TimelineEvent> = + list.filter { it.root.getClearType() == EventType.MESSAGE } + + private fun isRepliesVisibleFor(id: String) = repliesVisibleEvents.contains(id) + + private fun transformToGroupPosts(list: List<TimelineEvent>): List<Post> { + return list.mapNotNull { timelineEvent -> + getPostContentTypeFor(timelineEvent)?.let { contentType -> + timelineEvent.toPost(contentType, isRepliesVisibleFor(timelineEvent.eventId)) + } + } + } + + private fun getPostContentTypeFor(event: TimelineEvent): PostContentType? { + val messageType = event.root.getClearContent()?.toModel<MessageContent>()?.msgType + return PostContentType.values().firstOrNull { it.typeKey == messageType } + } + + private fun setupRootMessagesWithVisibleReplies(groupMessages: List<Post>): List<RootPost> { + val rootMessages = getOnlyRootMessages(groupMessages) + val replies = getOnlyRepliesMessages(groupMessages) + val list = rootMessages.map { message -> + val repliesForEvent = getRepliesFor(replies, message.id) + message.copy(replies = repliesForEvent) + } + return list + } + + private fun getOnlyRootMessages(list: List<Post>): List<RootPost> = + list.filterIsInstance<RootPost>() + + private fun getOnlyRepliesMessages(list: List<Post>): List<ReplyPost> = + list.filterIsInstance<ReplyPost>() + + private fun getRepliesFor(replies: List<ReplyPost>, eventId: String) = + replies.filter { eventId == it.replyToId } + +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/ui/groups/timeline/data_source/GroupTimelineDatasource.kt b/app/src/main/java/com/futo/circles/ui/groups/timeline/data_source/GroupTimelineDatasource.kt new file mode 100644 index 0000000000000000000000000000000000000000..1058d7f783f65223fe634d7a9665e8460d0643fc --- /dev/null +++ b/app/src/main/java/com/futo/circles/ui/groups/timeline/data_source/GroupTimelineDatasource.kt @@ -0,0 +1,62 @@ +package com.futo.circles.ui.groups.timeline.data_source + +import androidx.lifecycle.MutableLiveData +import com.futo.circles.extensions.nameOrId +import com.futo.circles.model.Post +import com.futo.circles.provider.MatrixSessionProvider +import org.matrix.android.sdk.api.session.room.timeline.Timeline +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings + +class GroupTimelineDatasource( + private val roomId: String, + private val matrixSessionProvider: MatrixSessionProvider, + private val timelineBuilder: GroupTimelineBuilder +) : Timeline.Listener { + + private val room = matrixSessionProvider.currentSession?.getRoom(roomId) + + val timelineEventsLiveData = MutableLiveData<List<Post>>() + + private var timeline: Timeline? = null + + fun getGroupTitle() = room?.roomSummary()?.nameOrId() ?: roomId + + fun startTimeline() { + timeline = room?.createTimeline(null, TimelineSettings(MESSAGES_PER_PAGE))?.apply { + addListener(this@GroupTimelineDatasource) + start() + } + } + + fun clearTimeline() { + timeline?.apply { + removeAllListeners() + dispose() + } + } + + fun loadMore() { + if (timeline?.hasMoreToLoad(Timeline.Direction.BACKWARDS) == true) + timeline?.paginate(Timeline.Direction.BACKWARDS, MESSAGES_PER_PAGE) + } + + fun toggleRepliesVisibility(eventId: String) { + timelineEventsLiveData.value = timelineBuilder.toggleRepliesVisibilityFor(eventId) + } + + override fun onTimelineUpdated(snapshot: List<TimelineEvent>) { + timelineEventsLiveData.value = timelineBuilder.build(snapshot) + } + + override fun onTimelineFailure(throwable: Throwable) { + timeline?.restartWithEventId(null) + } + + fun getUrlResolver() = matrixSessionProvider.currentSession?.contentUrlResolver() + + companion object { + private const val MESSAGES_PER_PAGE = 30 + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/ui/groups/timeline/list/GroupTimelineAdapter.kt b/app/src/main/java/com/futo/circles/ui/groups/timeline/list/GroupTimelineAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..c9245dc13a3243ea9a5e40fe5b7203c462fe9011 --- /dev/null +++ b/app/src/main/java/com/futo/circles/ui/groups/timeline/list/GroupTimelineAdapter.kt @@ -0,0 +1,61 @@ +package com.futo.circles.ui.groups.timeline.list + +import android.view.ViewGroup +import com.futo.circles.base.BaseRvAdapter +import com.futo.circles.model.Post +import com.futo.circles.model.PostContentType +import com.futo.circles.model.PostItemPayload +import com.futo.circles.model.RootPost +import com.futo.circles.ui.view.GroupPostListener +import org.matrix.android.sdk.api.session.content.ContentUrlResolver + +class GroupTimelineAdapter( + private val postListener: GroupPostListener, + private val urlResolver: ContentUrlResolver?, + private val onLoadMore: () -> Unit +) : BaseRvAdapter<Post, GroupPostViewHolder>(PayloadIdEntityCallback { _, new -> + (new as? RootPost)?.let { rootPost -> + PostItemPayload( + repliesCount = rootPost.getRepliesCount(), + isRepliesVisible = rootPost.isRepliesVisible, + hasReplies = rootPost.hasReplies() + ) + } +}) { + + + override fun getItemViewType(position: Int): Int { + return getItem(position).content.type.ordinal + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GroupPostViewHolder { + return when (PostContentType.values()[viewType]) { + PostContentType.TEXT_CONTENT -> TextPostViewHolder(parent, postListener, urlResolver) + PostContentType.IMAGE_CONTENT -> ImagePostViewHolder(parent, postListener, urlResolver) + } + } + + override fun onBindViewHolder(holder: GroupPostViewHolder, position: Int) { + holder.bind(getItem(position)) + if (position >= itemCount - LOAD_MORE_THRESHOLD) onLoadMore() + } + + override fun onBindViewHolder( + holder: GroupPostViewHolder, + position: Int, + payloads: MutableList<Any> + ) { + if (payloads.isNullOrEmpty()) { + super.onBindViewHolder(holder, position, payloads) + } else { + payloads.forEach { + (it as? PostItemPayload)?.let { payload -> holder.bindPayload(payload) } + } + } + } + + companion object { + private const val LOAD_MORE_THRESHOLD = 10 + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/ui/groups/timeline/list/GroupTimelineViewHolder.kt b/app/src/main/java/com/futo/circles/ui/groups/timeline/list/GroupTimelineViewHolder.kt new file mode 100644 index 0000000000000000000000000000000000000000..af5a82640a1a21d47ed55a8c4e4fb054ca4a9d33 --- /dev/null +++ b/app/src/main/java/com/futo/circles/ui/groups/timeline/list/GroupTimelineViewHolder.kt @@ -0,0 +1,78 @@ +package com.futo.circles.ui.groups.timeline.list + +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.futo.circles.base.ViewBindingHolder +import com.futo.circles.databinding.ImagePostViewBinding +import com.futo.circles.databinding.TextPostViewBinding +import com.futo.circles.extensions.loadMatrixThumbnail +import com.futo.circles.model.ImageContent +import com.futo.circles.model.Post +import com.futo.circles.model.PostItemPayload +import com.futo.circles.model.TextContent +import com.futo.circles.ui.view.GroupPostListener +import com.futo.circles.ui.view.PostLayout +import org.matrix.android.sdk.api.session.content.ContentUrlResolver + +sealed class GroupPostViewHolder(view: View, private val urlResolver: ContentUrlResolver?) : + RecyclerView.ViewHolder(view) { + + abstract val postLayout: PostLayout + + open fun bind(post: Post) { + postLayout.setData(post, urlResolver) + } + + fun bindPayload(payload: PostItemPayload) { + postLayout.setPayload(payload) + } +} + +class TextPostViewHolder( + parent: ViewGroup, + postListener: GroupPostListener, + private val urlResolver: ContentUrlResolver? +) : GroupPostViewHolder(inflate(parent, TextPostViewBinding::inflate), urlResolver) { + + private companion object : ViewBindingHolder + + private val binding = baseBinding as TextPostViewBinding + override val postLayout: PostLayout = binding.lTextPost + + init { + binding.lTextPost.setListener(postListener) + } + + override fun bind(post: Post) { + super.bind(post) + + (post.content as? TextContent)?.let { + binding.tvContent.text = it.message + } + } +} + +class ImagePostViewHolder( + parent: ViewGroup, + postListener: GroupPostListener, + private val urlResolver: ContentUrlResolver? +) : GroupPostViewHolder(inflate(parent, ImagePostViewBinding::inflate), urlResolver) { + + private companion object : ViewBindingHolder + + private val binding = baseBinding as ImagePostViewBinding + override val postLayout: PostLayout = binding.lImagePost + + init { + binding.lImagePost.setListener(postListener) + } + + override fun bind(post: Post) { + super.bind(post) + + (post.content as? ImageContent)?.let { + binding.ivContent.loadMatrixThumbnail(it.url, urlResolver) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/ui/log_in/data_source/LoginDataSource.kt b/app/src/main/java/com/futo/circles/ui/log_in/data_source/LoginDataSource.kt index 4602e672ae87ef4eb695ff42d7beea33d3e65ab6..2e809484ac6a17cab4b3c8bb803d1e88498ea703 100644 --- a/app/src/main/java/com/futo/circles/ui/log_in/data_source/LoginDataSource.kt +++ b/app/src/main/java/com/futo/circles/ui/log_in/data_source/LoginDataSource.kt @@ -1,23 +1,30 @@ package com.futo.circles.ui.log_in.data_source +import android.content.Context +import com.futo.circles.R import com.futo.circles.extensions.createResult import com.futo.circles.provider.MatrixHomeServerProvider -import com.futo.circles.provider.MatrixProvider import com.futo.circles.provider.MatrixSessionProvider -import java.util.* +import org.matrix.android.sdk.api.Matrix -class LoginDataSource(private val matrixSessionProvider: MatrixSessionProvider) { +class LoginDataSource( + private val context: Context, + private val matrixSessionProvider: MatrixSessionProvider +) { suspend fun logIn(name: String, password: String, secondPassword: String?) = createResult { val homeServerConnectionConfig = MatrixHomeServerProvider().createHomeServerConfig() - MatrixProvider.matrix.authenticationService().directAuthentication( + Matrix.getInstance(context).authenticationService().directAuthentication( homeServerConnectionConfig = homeServerConnectionConfig, matrixId = name, password = password, deviceId = secondPassword, - initialDeviceName = UUID.randomUUID().toString() + initialDeviceName = context.getString( + R.string.initial_device_name, + context.getString(R.string.app_name) + ) ).also { matrixSessionProvider.startSession(it) } } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/ui/view/AdvancedOptionsView.kt b/app/src/main/java/com/futo/circles/ui/view/AdvancedOptionsView.kt index 791bcf3874f7e1af0284a4ed5a5a506f4a3019a7..b4a5fa6f3a876be2b20e3b3071a8b957527bd1a8 100644 --- a/app/src/main/java/com/futo/circles/ui/view/AdvancedOptionsView.kt +++ b/app/src/main/java/com/futo/circles/ui/view/AdvancedOptionsView.kt @@ -8,6 +8,7 @@ import androidx.core.view.isVisible import com.futo.circles.R import com.futo.circles.databinding.AdvancedOptionsViewBinding import com.futo.circles.extensions.gone +import com.futo.circles.extensions.setVisibility import com.futo.circles.extensions.visible class AdvancedOptionsView( @@ -28,19 +29,9 @@ class AdvancedOptionsView( binding.tilPassword.editText?.text.toString().takeIf { it.isNotEmpty() } private fun toggleEncryptionPasswordVisibility() { - if (binding.tilPassword.isVisible) { - binding.tilPassword.gone() - binding.btnAdvanced.apply { - text = context.getString(R.string.advanced_options) - setIconResource(R.drawable.ic_keyboard_arrow_right) - } - } else { - binding.tilPassword.visible() - binding.btnAdvanced.apply { - text = context.getString(R.string.hide_advanced_options) - setIconResource(R.drawable.ic_keyboard_arrow_down) - } - } + val isOpened = binding.btnAdvanced.isOpened() + binding.tilPassword.setVisibility(!isOpened) + binding.btnAdvanced.setIsOpened(!isOpened) } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/ui/view/ExpandContentButton.kt b/app/src/main/java/com/futo/circles/ui/view/ExpandContentButton.kt new file mode 100644 index 0000000000000000000000000000000000000000..0e126b9e3cef5a8e55d3640611395f6e62f804a6 --- /dev/null +++ b/app/src/main/java/com/futo/circles/ui/view/ExpandContentButton.kt @@ -0,0 +1,65 @@ +package com.futo.circles.ui.view + +import android.content.Context +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import com.futo.circles.R +import com.futo.circles.extensions.getAttributes +import com.google.android.material.button.MaterialButton + +class ExpandContentButton( + context: Context, + attrs: AttributeSet? = null, +) : MaterialButton(context, attrs) { + + private var openedText: String = "" + private var closedText: String = "" + + private var openedIcon: Drawable? = null + private var closedIcon: Drawable? = null + + private var isOpened: Boolean = false + + init { + getAttributes(attrs, R.styleable.ExpandContentButton) { + getText(R.styleable.ExpandContentButton_closed_text)?.let { + text = it + closedText = it.toString() + } + openedText = getText(R.styleable.ExpandContentButton_opened_text)?.toString() ?: "" + + getDrawable(R.styleable.ExpandContentButton_closed_icon)?.let { + icon = it + closedIcon = it + } + + getDrawable(R.styleable.ExpandContentButton_opened_icon)?.let { + openedIcon = it + } + } + } + + fun setClosedText(title: String) { + text = title.also { closedText = it } + } + + fun setIsOpened(isOpened: Boolean) { + if (isOpened) open() + else close() + } + + fun isOpened() = isOpened + + private fun open() { + isOpened = true + text = openedText + icon = openedIcon + } + + private fun close() { + isOpened = false + text = closedText + icon = closedIcon + } +} + diff --git a/app/src/main/java/com/futo/circles/ui/view/GroupPostFooterView.kt b/app/src/main/java/com/futo/circles/ui/view/GroupPostFooterView.kt new file mode 100644 index 0000000000000000000000000000000000000000..71d4e9e399bcc16af638cbf4cb4eb946435bc60f --- /dev/null +++ b/app/src/main/java/com/futo/circles/ui/view/GroupPostFooterView.kt @@ -0,0 +1,31 @@ +package com.futo.circles.ui.view + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.constraintlayout.widget.ConstraintLayout +import com.futo.circles.databinding.GroupPostFooterViewBinding +import com.futo.circles.extensions.setIsEncryptedIcon +import com.futo.circles.extensions.setVisibility +import com.futo.circles.model.PostInfo +import java.text.DateFormat +import java.util.* + + +class GroupPostFooterView( + context: Context, + attrs: AttributeSet? = null, +) : ConstraintLayout(context, attrs) { + + private val binding = + GroupPostFooterViewBinding.inflate(LayoutInflater.from(context), this) + + fun setData(data: PostInfo, isReply:Boolean) { + with(binding){ + btnReply.setVisibility(!isReply) + ivEncrypted.setIsEncryptedIcon(data.isEncrypted) + tvMessageTime.text = DateFormat.getDateTimeInstance().format(Date(data.timestamp)) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/ui/view/GroupPostHeaderView.kt b/app/src/main/java/com/futo/circles/ui/view/GroupPostHeaderView.kt new file mode 100644 index 0000000000000000000000000000000000000000..66155df99890c55c6ceb477ffda2264241b629cc --- /dev/null +++ b/app/src/main/java/com/futo/circles/ui/view/GroupPostHeaderView.kt @@ -0,0 +1,30 @@ +package com.futo.circles.ui.view + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.constraintlayout.widget.ConstraintLayout +import com.futo.circles.databinding.GroupPostHeaderViewBinding +import com.futo.circles.extensions.loadMatrixThumbnail +import org.matrix.android.sdk.api.session.content.ContentUrlResolver +import org.matrix.android.sdk.api.session.room.sender.SenderInfo + +class GroupPostHeaderView( + context: Context, + attrs: AttributeSet? = null, +) : ConstraintLayout(context, attrs) { + + private val binding = + GroupPostHeaderViewBinding.inflate(LayoutInflater.from(context), this) + + fun setData(sender: SenderInfo, urlResolver: ContentUrlResolver?) { + binding.ivSenderImage.loadMatrixThumbnail( + sender.avatarUrl, + urlResolver, + binding.ivSenderImage.height + ) + binding.tvUserName.text = sender.disambiguatedDisplayName + binding.tvUserId.text = sender.userId + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/ui/view/LoadingButton.kt b/app/src/main/java/com/futo/circles/ui/view/LoadingButton.kt index f4d761e427b977730dc19ae107799280a9dd8337..14bb59eb126bd93222914322e27f65957f85969c 100644 --- a/app/src/main/java/com/futo/circles/ui/view/LoadingButton.kt +++ b/app/src/main/java/com/futo/circles/ui/view/LoadingButton.kt @@ -21,8 +21,9 @@ class LoadingButton( init { getAttributes(attrs, R.styleable.LoadingButton) { - getResourceId(R.styleable.LoadingButton_android_text, 0).takeIf { it != 0 }?.let { - buttonText = context.getString(it).also { binding.button.text = it } + getText(R.styleable.LoadingButton_android_text)?.let { + buttonText = it.toString() + binding.button.text = it } } } diff --git a/app/src/main/java/com/futo/circles/ui/view/PostLayout.kt b/app/src/main/java/com/futo/circles/ui/view/PostLayout.kt new file mode 100644 index 0000000000000000000000000000000000000000..2b8151253ba2c78253c71ad38902797e52e96bcc --- /dev/null +++ b/app/src/main/java/com/futo/circles/ui/view/PostLayout.kt @@ -0,0 +1,99 @@ +package com.futo.circles.ui.view + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.constraintlayout.widget.ConstraintLayout +import com.futo.circles.R +import com.futo.circles.databinding.PostLayoutBinding +import com.futo.circles.extensions.gone +import com.futo.circles.extensions.setVisibility +import com.futo.circles.model.Post +import com.futo.circles.model.PostItemPayload +import com.futo.circles.model.ReplyPost +import com.futo.circles.model.RootPost +import org.matrix.android.sdk.api.session.content.ContentUrlResolver + + +interface GroupPostListener { + fun onShowRepliesClicked(eventId: String) +} + +class PostLayout( + context: Context, + attrs: AttributeSet? = null, +) : ConstraintLayout(context, attrs) { + + private val binding = + PostLayoutBinding.inflate(LayoutInflater.from(context), this) + + private var listener: GroupPostListener? = null + private var post: Post? = null + + init { + binding.btnShowReplies.setOnClickListener { + post?.let { listener?.onShowRepliesClicked(it.id) } + } + } + + fun setListener(groupPostListener: GroupPostListener) { + listener = groupPostListener + } + + + fun setData(data: Post, urlResolver: ContentUrlResolver?) { + post = data + setGeneralMessageData(data, urlResolver) + bindRepliesButton(data) + } + + fun setPayload(payload: PostItemPayload) { + bindRepliesButton(payload.hasReplies, payload.repliesCount, payload.isRepliesVisible) + } + + private fun setGeneralMessageData( + data: Post, + urlResolver: ContentUrlResolver? + ) { + val isReply = data is ReplyPost + binding.vReplyMargin.setVisibility(isReply) + binding.postHeader.setData(data.postInfo.sender, urlResolver) + binding.postFooter.setData(data.postInfo, isReply) + } + + private fun bindRepliesButton(post: Post) { + val rootPost = (post as? RootPost) ?: kotlin.run { binding.btnShowReplies.gone(); return } + + bindRepliesButton( + rootPost.hasReplies(), rootPost.getRepliesCount(), rootPost.isRepliesVisible + ) + } + + private fun bindRepliesButton( + hasReplies: Boolean, + repliesCount: Int, + isRepliesVisible: Boolean + ) { + with(binding.btnShowReplies) { + setVisibility(hasReplies) + setClosedText( + context.resources.getQuantityString( + R.plurals.show__replies_plurals, + repliesCount, repliesCount + ) + ) + setIsOpened(isRepliesVisible) + } + } + + override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams?) { + if (child.id == R.id.postCard || child.id == R.id.btnShowReplies || child.id == R.id.vReplyMargin) { + super.addView(child, index, params) + } else { + findViewById<FrameLayout>(R.id.lvContent).addView(child, index, params) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_create.xml b/app/src/main/res/drawable/ic_create.xml new file mode 100644 index 0000000000000000000000000000000000000000..faddfce421038a77e3a7e6658df78cdae0e1848f --- /dev/null +++ b/app/src/main/res/drawable/ic_create.xml @@ -0,0 +1,5 @@ +<vector android:height="24dp" android:tint="#FFFFFF" + android:viewportHeight="24" android:viewportWidth="24" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="@android:color/white" android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_like.xml b/app/src/main/res/drawable/ic_like.xml new file mode 100644 index 0000000000000000000000000000000000000000..2d9d13cfbf28628607689fa8f62ec7ef3fc4b2c5 --- /dev/null +++ b/app/src/main/res/drawable/ic_like.xml @@ -0,0 +1,5 @@ +<vector android:height="24dp" android:tint="#FFFFFF" + android:viewportHeight="24" android:viewportWidth="24" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="@android:color/white" android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_more.xml b/app/src/main/res/drawable/ic_more.xml new file mode 100644 index 0000000000000000000000000000000000000000..8bb3fc306f2440057586e0f419521985059b4186 --- /dev/null +++ b/app/src/main/res/drawable/ic_more.xml @@ -0,0 +1,5 @@ +<vector android:height="24dp" android:tint="#FFFFFF" + android:viewportHeight="24" android:viewportWidth="24" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="@android:color/white" android:pathData="M6,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_reply.xml b/app/src/main/res/drawable/ic_reply.xml new file mode 100644 index 0000000000000000000000000000000000000000..f79c9c8ef6610ccbe731db4d28b101bfbed344a2 --- /dev/null +++ b/app/src/main/res/drawable/ic_reply.xml @@ -0,0 +1,5 @@ +<vector android:height="24dp" android:tint="#FFFFFF" + android:viewportHeight="24" android:viewportWidth="24" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="@android:color/white" android:pathData="M20,2L4,2c-1.1,0 -2,0.9 -2,2v18l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,16L6,16l-2,2L4,4h16v12z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml new file mode 100644 index 0000000000000000000000000000000000000000..d0d075a1f76d90fc6f7728d3a26a0f4bdb62badb --- /dev/null +++ b/app/src/main/res/drawable/ic_share.xml @@ -0,0 +1,5 @@ +<vector android:height="24dp" android:tint="#FFFFFF" + android:viewportHeight="24" android:viewportWidth="24" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="@android:color/white" android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92s2.92,-1.31 2.92,-2.92c0,-1.61 -1.31,-2.92 -2.92,-2.92zM18,4c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM6,13c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM18,20.02c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_unlike.xml b/app/src/main/res/drawable/ic_unlike.xml new file mode 100644 index 0000000000000000000000000000000000000000..996e16d0f381e05453e40db5bc53f391c1b5b63e --- /dev/null +++ b/app/src/main/res/drawable/ic_unlike.xml @@ -0,0 +1,5 @@ +<vector android:height="24dp" android:tint="#FFFFFF" + android:viewportHeight="24" android:viewportWidth="24" + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="@android:color/white" android:pathData="M16.5,3c-1.74,0 -3.41,0.81 -4.5,2.09C10.91,3.81 9.24,3 7.5,3 4.42,3 2,5.42 2,8.5c0,3.78 3.4,6.86 8.55,11.54L12,21.35l1.45,-1.32C18.6,15.36 22,12.28 22,8.5 22,5.42 19.58,3 16.5,3zM12.1,18.55l-0.1,0.1 -0.1,-0.1C7.14,14.24 4,11.39 4,8.5 4,6.5 5.5,5 7.5,5c1.54,0 3.04,0.99 3.57,2.36h1.87C13.46,5.99 14.96,5 16.5,5c2,0 3.5,1.5 3.5,3.5 0,2.89 -3.14,5.74 -7.9,10.05z"/> +</vector> diff --git a/app/src/main/res/layout/advanced_options_view.xml b/app/src/main/res/layout/advanced_options_view.xml index 4b910a8d8462c3e1d80ef14aac9f83a5375a9b11..f3870d3292f204786e92d3a3afa8ba9f815ddf49 100644 --- a/app/src/main/res/layout/advanced_options_view.xml +++ b/app/src/main/res/layout/advanced_options_view.xml @@ -6,17 +6,21 @@ android:layout_height="wrap_content" tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> - <com.google.android.material.button.MaterialButton + <com.futo.circles.ui.view.ExpandContentButton android:id="@+id/btnAdvanced" style="@style/Widget.MaterialComponents.Button.TextButton.Icon" - android:layout_width="0dp" + android:layout_width="wrap_content" android:layout_height="wrap_content" android:minHeight="0dp" android:paddingStart="0dp" android:paddingEnd="40dp" - android:text="@string/advanced_options" android:textAppearance="@style/footNote" - app:icon="@drawable/ic_keyboard_arrow_right" + app:closed_icon="@drawable/ic_keyboard_arrow_right" + app:closed_text="@string/advanced_options" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:opened_icon="@drawable/ic_keyboard_arrow_down" + app:opened_text="@string/hide_advanced_options" tools:ignore="RtlSymmetry" /> <com.google.android.material.textfield.TextInputLayout diff --git a/app/src/main/res/layout/bottom_navigation_fragment.xml b/app/src/main/res/layout/bottom_navigation_fragment.xml index 6d1f9e5a8cc62e1b367bcf093cf70526d7b649e9..73c37dab729b2586c3986a95e277b348f6246073 100644 --- a/app/src/main/res/layout/bottom_navigation_fragment.xml +++ b/app/src/main/res/layout/bottom_navigation_fragment.xml @@ -48,6 +48,7 @@ android:id="@+id/bottomNavigationView" android:layout_width="0dp" android:layout_height="wrap_content" + app:itemBackground="?selectableItemBackgroundBorderless" app:labelVisibilityMode="labeled" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" diff --git a/app/src/main/res/layout/group_post_footer_view.xml b/app/src/main/res/layout/group_post_footer_view.xml new file mode 100644 index 0000000000000000000000000000000000000000..92959590f3a07a9a6b1358d9e036bf25096a7df3 --- /dev/null +++ b/app/src/main/res/layout/group_post_footer_view.xml @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="utf-8"?> +<merge xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> + + <ImageView + android:id="@+id/ivEncrypted" + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintBottom_toBottomOf="@id/tvMessageTime" + app:layout_constraintDimensionRatio="h,1:1" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@id/tvMessageTime" + tools:src="@drawable/ic_lock" /> + + <TextView + android:id="@+id/tvMessageTime" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:ellipsize="end" + android:lines="1" + android:textSize="12sp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/ivEncrypted" + app:layout_constraintTop_toTopOf="parent" + tools:text="some date" /> + + <View + android:id="@+id/divider" + android:layout_width="0dp" + android:layout_height="@dimen/divider_height" + android:layout_marginTop="8dp" + android:background="@color/divider_color" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/tvMessageTime" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/btnLike" + style="@style/PostButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/like" + app:icon="@drawable/ic_unlike" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/btnReply" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/divider" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/btnReply" + style="@style/PostButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/reply" + app:icon="@drawable/ic_reply" + app:layout_constraintBottom_toBottomOf="@id/btnLike" + app:layout_constraintEnd_toStartOf="@id/btnShare" + app:layout_constraintStart_toEndOf="@id/btnLike" + app:layout_constraintTop_toTopOf="@id/btnLike" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/btnShare" + style="@style/PostButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/share" + app:icon="@drawable/ic_share" + app:layout_constraintBottom_toBottomOf="@id/btnLike" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/btnReply" + app:layout_constraintTop_toTopOf="@id/btnLike" /> + + +</merge> \ No newline at end of file diff --git a/app/src/main/res/layout/group_post_header_view.xml b/app/src/main/res/layout/group_post_header_view.xml new file mode 100644 index 0000000000000000000000000000000000000000..2245cd65a7c7c1ed2b59856c6f744e30541cb756 --- /dev/null +++ b/app/src/main/res/layout/group_post_header_view.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="utf-8"?> +<merge xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> + + <com.google.android.material.imageview.ShapeableImageView + android:id="@+id/ivSenderImage" + android:layout_width="40dp" + android:layout_height="0dp" + app:layout_constraintDimensionRatio="w,1:1" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.App.CornerSize50Percent" + tools:background="@color/blue" /> + + <TextView + android:id="@+id/tvUserName" + style="@style/body" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" + android:ellipsize="end" + android:lines="1" + app:layout_constraintBottom_toTopOf="@id/tvUserId" + app:layout_constraintEnd_toStartOf="@id/btnMore" + app:layout_constraintStart_toEndOf="@id/ivSenderImage" + app:layout_constraintTop_toTopOf="@id/ivSenderImage" + app:layout_constraintVertical_chainStyle="packed" + tools:text="Android01" /> + + <TextView + android:id="@+id/tvUserId" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:ellipsize="end" + android:lines="1" + android:textSize="13sp" + app:layout_constraintBottom_toBottomOf="@id/ivSenderImage" + app:layout_constraintEnd_toEndOf="@id/tvUserName" + app:layout_constraintStart_toStartOf="@id/tvUserName" + app:layout_constraintTop_toBottomOf="@id/tvUserName" + tools:text="Android01@domain" /> + + <ImageButton + android:id="@+id/btnMore" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="?selectableItemBackgroundBorderless" + android:paddingHorizontal="4dp" + android:paddingBottom="2dp" + android:src="@drawable/ic_more" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:tint="@color/blue" /> + +</merge> \ No newline at end of file diff --git a/app/src/main/res/layout/group_timeline_fragment.xml b/app/src/main/res/layout/group_timeline_fragment.xml index 77d9ef65f8c7c6bf54bdef7d54d1646b86417404..5873afd039dcc9ebdd03989cbf5efda09b0fd34d 100644 --- a/app/src/main/res/layout/group_timeline_fragment.xml +++ b/app/src/main/res/layout/group_timeline_fragment.xml @@ -1,6 +1,28 @@ <?xml version="1.0" encoding="utf-8"?> -<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> -</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/tvGroupTimeline" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipToPadding="false" + android:clipChildren="false" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> + + + <com.google.android.material.floatingactionbutton.FloatingActionButton + android:id="@+id/fbCreatePost" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="bottom|end" + android:layout_margin="16dp" + app:fabSize="normal" + app:layout_anchor="@id/tvGroupTimeline" + app:srcCompat="@drawable/ic_create" + app:tint="@color/white" + tools:ignore="ContentDescription" /> +</FrameLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/image_post_view.xml b/app/src/main/res/layout/image_post_view.xml new file mode 100644 index 0000000000000000000000000000000000000000..460a022b54cc3cae37bca66c625f8d0adc73502e --- /dev/null +++ b/app/src/main/res/layout/image_post_view.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<com.futo.circles.ui.view.PostLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/lImagePost" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <ImageView + android:id="@+id/ivContent" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + +</com.futo.circles.ui.view.PostLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/post_layout.xml b/app/src/main/res/layout/post_layout.xml new file mode 100644 index 0000000000000000000000000000000000000000..74f47e39c5a97792baa25d0e1137fa01003d08fc --- /dev/null +++ b/app/src/main/res/layout/post_layout.xml @@ -0,0 +1,105 @@ +<?xml version="1.0" encoding="utf-8"?> +<merge xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> + + <View + android:id="@+id/vReplyMargin" + android:layout_width="@dimen/reply_post_item_margin" + android:layout_height="0dp" + android:visibility="gone" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + + <androidx.cardview.widget.CardView + android:id="@+id/postCard" + android:layout_width="0dp" + android:layout_height="wrap_content" + app:cardCornerRadius="4dp" + app:cardElevation="8dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/vReplyMargin" + app:layout_constraintTop_toTopOf="parent"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/guidelineStart" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_begin="8dp" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/guidelineEnd" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_end="8dp" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/guidelineTop" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_begin="8dp" /> + + <com.futo.circles.ui.view.GroupPostHeaderView + android:id="@+id/postHeader" + android:layout_width="0dp" + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="@id/guidelineEnd" + app:layout_constraintStart_toStartOf="@id/guidelineStart" + app:layout_constraintTop_toTopOf="@id/guidelineTop" /> + + + <FrameLayout + android:id="@+id/lvContent" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/postHeader" /> + + + <com.futo.circles.ui.view.GroupPostFooterView + android:id="@+id/postFooter" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="@id/guidelineEnd" + app:layout_constraintStart_toStartOf="@id/guidelineStart" + app:layout_constraintTop_toBottomOf="@id/lvContent" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + </androidx.cardview.widget.CardView> + + <com.futo.circles.ui.view.ExpandContentButton + android:id="@+id/btnShowReplies" + style="@style/Widget.MaterialComponents.Button.TextButton.Icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="8dp" + android:minHeight="0dp" + android:textAppearance="@style/footNote" + android:visibility="gone" + app:closed_icon="@drawable/ic_keyboard_arrow_right" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/postCard" + app:opened_icon="@drawable/ic_keyboard_arrow_down" + app:opened_text="@string/hide_replies" + tools:ignore="RtlSymmetry" + tools:visibility="visible" /> + +</merge> \ No newline at end of file diff --git a/app/src/main/res/layout/text_post_view.xml b/app/src/main/res/layout/text_post_view.xml new file mode 100644 index 0000000000000000000000000000000000000000..c11aade96b932a5d1d33e47e7e8a803f752f046a --- /dev/null +++ b/app/src/main/res/layout/text_post_view.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<com.futo.circles.ui.view.PostLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/lTextPost" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/tvContent" + style="@style/body" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/post_text_side_margin" /> + +</com.futo.circles.ui.view.PostLayout> \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 480b4595c170199009c22c6132f7f6f21ed147cb..eee4ca85557fae5864f9e9ffaaafeac8836cf665 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -5,4 +5,11 @@ <attr name="android:text" /> </declare-styleable> + <declare-styleable name="ExpandContentButton"> + <attr name="closed_text" format="string" /> + <attr name="opened_text" format="string" /> + <attr name="closed_icon" format="reference" /> + <attr name="opened_icon" format="reference" /> + </declare-styleable> + </resources> \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index aa48f84f6246b1f70549b5c7d2a3f5d70488aef4..1c5fa8ee8a31bf1a0fe00d36e39464b1aedeaeb2 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -4,7 +4,6 @@ <color name="purple_500">#FF6200EE</color> <color name="purple_700">#FF3700B3</color> - <color name="teal_200">#FF03DAC5</color> <color name="teal_700">#FF018786</color> <color name="black">#FF000000</color> diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index e6d1263553903a59150ae19519a5b8552d8d880c..fa720bdd17e56bd896cb64a24603d3b1fec935b4 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,4 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <resources> <dimen name="divider_height">1dp</dimen> + <dimen name="group_post_item_offset">4dp</dimen> + <dimen name="reply_post_item_margin">24dp</dimen> + <dimen name="post_text_side_margin">24dp</dimen> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b13859d89244b3b1c4853b46f44fd7871eeebe75..6e2137b5289527d5e89a7729edfba4c60d3f2a08 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -21,9 +21,19 @@ <string name="my_people">My People</string> <string name="my_circles">My Circles</string> <string name="photo_galleries">Photo Galleries</string> + <string name="initial_device_name">%s (Android)</string> + <string name="like">Like</string> + <string name="reply">Reply</string> + <string name="share">Share</string> + <string name="hide_replies">Hide replies</string> <plurals name="member_plurals"> <item quantity="one">%d member</item> <item quantity="other">%d members</item> </plurals> + + <plurals name="show__replies_plurals"> + <item quantity="one">Show %d reply</item> + <item quantity="other">Show %d replies</item> + </plurals> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index f2feacee049c94f0227eb8099d580bc5b872e5d7..fdc19c79587083be35a5ba8ab360bdf6094f5eac 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -8,6 +8,16 @@ <item name="android:textSize">17sp</item> </style> + <style name="PostButtonStyle" parent="@style/Widget.MaterialComponents.Button.TextButton.Icon"> + <item name="textAllCaps">false</item> + <item name="cornerRadius">8dp</item> + <item name="android:textSize">15sp</item> + <item name="iconPadding">4dp</item> + <item name="iconSize">24dp</item> + <item name="android:padding">4dp</item> + <item name="android:minHeight">0dp</item> + </style> + <style name="headline" parent="TextAppearance.MaterialComponents.Headline6"> <item name="android:textStyle">bold</item> <item name="android:textSize">17sp</item> @@ -38,4 +48,8 @@ <item name="android:textColor">@color/gray</item> <item name="android:textSize">15sp</item> </style> + + <style name="ShapeAppearanceOverlay.App.CornerSize50Percent" parent=""> + <item name="cornerSize">50%</item> + </style> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index b21c15d523bb740c344362cd7d1f50011f35687d..d7bf13054ddff5ce94281bea2d6eb72d00cf9041 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -6,10 +6,11 @@ <item name="colorPrimaryVariant">@color/purple_700</item> <item name="colorOnPrimary">@color/white</item> <!-- Secondary brand color. --> - <item name="colorSecondary">@color/teal_200</item> + <item name="colorSecondary">@color/blue</item> <item name="colorSecondaryVariant">@color/teal_700</item> <item name="colorOnSecondary">@color/black</item> + <item name="colorControlHighlight">#330E7AFE</item> <item name="android:statusBarColor">@color/status_bar_color</item> </style>