diff --git a/app/build.gradle b/app/build.gradle index b13c3d79ffc949136d00879bc2f4d8d8f2ea281e..eb41d9368c04fdfc651b976ab4d55b836eadbeb1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,7 +29,7 @@ android { dimension flavor_dimension_name applicationIdSuffix ".stage" resValue "string", "app_name", "Circles Stage" - buildConfigField("String", "MATRIX_HOME_SERVER_URL", "\"https://matrix.kombucha.social/\"") + buildConfigField("String", "MATRIX_HOME_SERVER_URL", "\"https://matrix.eu.kombucha.social/\"") } prod { dimension flavor_dimension_name @@ -84,12 +84,15 @@ dependencies { implementation "androidx.navigation:navigation-ui-ktx:$rootProject.ext.nav_version" //ViewBinding - implementation 'com.github.kirich1409:viewbindingpropertydelegate:1.5.6' + implementation 'com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.6' //Matrix implementation 'org.matrix.android:matrix-android-sdk2:1.3.18' implementation 'com.squareup.okhttp3:okhttp:4.9.3' + //Picasso + implementation 'com.squareup.picasso:picasso:2.71828' + //test testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' diff --git a/app/src/main/java/com/futo/circles/base/BaseRecyclerView.kt b/app/src/main/java/com/futo/circles/base/BaseRecyclerView.kt new file mode 100644 index 0000000000000000000000000000000000000000..09f7511aae85d929466b873633c1deee73b1c220 --- /dev/null +++ b/app/src/main/java/com/futo/circles/base/BaseRecyclerView.kt @@ -0,0 +1,46 @@ +package com.futo.circles.base + +import android.annotation.SuppressLint +import android.content.Context +import android.view.LayoutInflater +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 + + protected constructor( + parent: ViewGroup, + inflate: (LayoutInflater, ViewGroup?, Boolean) -> VB + ) : this(inflate.invoke(LayoutInflater.from(parent.context), parent, false)) + + private constructor(viewBinding: VB) : super(viewBinding.root) { + this.binding = viewBinding + } + + abstract fun bind(data: T) +} + +val RecyclerView.ViewHolder.context: Context get() = this.itemView.context + +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 + override fun areContentsTheSame(oldItem: T, newItem: T): Boolean = oldItem == newItem + } + } + +} 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 c4e91f1ca3d2c663fb06e619352b711c03c05439..e57fbe50965985dc0e9c6251a4f7d49d5524ef7e 100644 --- a/app/src/main/java/com/futo/circles/di/UiModule.kt +++ b/app/src/main/java/com/futo/circles/di/UiModule.kt @@ -1,9 +1,13 @@ package com.futo.circles.di +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.dsl.module val uiModule = module { viewModel { LogInViewModel(get()) } + viewModel { GroupsViewModel(get()) } + viewModel { (roomId: String) -> GroupTimelineViewModel(roomId, get()) } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/extensions/FragmentExtensions.kt b/app/src/main/java/com/futo/circles/extensions/FragmentExtensions.kt index db6c3546f8b6739f937fb75c2e4c7b80c19fbbd2..a6defc9a77151790de5bb218e627ed980bf32f25 100644 --- a/app/src/main/java/com/futo/circles/extensions/FragmentExtensions.kt +++ b/app/src/main/java/com/futo/circles/extensions/FragmentExtensions.kt @@ -6,6 +6,8 @@ import android.view.Gravity import android.view.ViewGroup import android.widget.FrameLayout import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar import androidx.core.view.children import androidx.fragment.app.Fragment import com.futo.circles.R @@ -37,14 +39,22 @@ fun Fragment.showError(message: String) { fun Fragment.setEnabledViews(enabled: Boolean) { (view?.rootView as? ViewGroup)?.children?.forEach { - if(it.isClickable) it.isEnabled = enabled + if (it.isClickable) it.isEnabled = enabled (it as? ViewGroup)?.setEnabledChildren(enabled) } } fun ViewGroup.setEnabledChildren(enabled: Boolean) { children.forEach { - if(it.isClickable) it.isEnabled = enabled + if (it.isClickable) it.isEnabled = enabled (it as? ViewGroup)?.setEnabledChildren(enabled) } +} + +fun Fragment.setSupportActionBar(toolbar: Toolbar) { + (activity as? AppCompatActivity)?.setSupportActionBar(toolbar) +} + +fun Fragment.setToolbarTitle(title: String) { + (activity as? AppCompatActivity)?.supportActionBar?.title = title } \ 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 new file mode 100644 index 0000000000000000000000000000000000000000..fb1f33b9e18e2c695db1a49b5e51d13698a556b0 --- /dev/null +++ b/app/src/main/java/com/futo/circles/extensions/ImageViewExtensions.kt @@ -0,0 +1,20 @@ +package com.futo.circles.extensions + +import android.widget.ImageView +import com.squareup.picasso.Picasso +import org.matrix.android.sdk.api.session.content.ContentUrlResolver + +private const val THUMBNAIL_SIZE = 250 + +fun ImageView.loadMatrixThumbnail( + avatarUrl: String?, + resolver: ContentUrlResolver?, + size: Int = THUMBNAIL_SIZE +) { + val resolvedUrl = resolver?.resolveThumbnail( + avatarUrl, + size, size, + ContentUrlResolver.ThumbnailMethod.SCALE + ) + Picasso.get().load(resolvedUrl).into(this) +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/extensions/LiveDataExtensions.kt b/app/src/main/java/com/futo/circles/extensions/LiveDataExtensions.kt index 38afe2a97c31fe3a32aa40bbc1043197390c1008..99085e3b677b6ea35ed5f750c8c4735f6742cbbc 100644 --- a/app/src/main/java/com/futo/circles/extensions/LiveDataExtensions.kt +++ b/app/src/main/java/com/futo/circles/extensions/LiveDataExtensions.kt @@ -2,6 +2,7 @@ package com.futo.circles.extensions import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer import org.matrix.android.sdk.api.failure.Failure fun <T> LiveData<Response<T>>.observeResponse( @@ -21,6 +22,13 @@ fun <T> LiveData<Response<T>>.observeResponse( } } +fun <T> LiveData<T>.observeData(fragment: Fragment, observer: (T) -> Unit) { + val owner = fragment.viewLifecycleOwner + observe(owner, Observer { + if (it != null) observer(it) + }) +} + suspend fun <T> createResult(block: suspend () -> T): Response<T> { return try { Response.Success(block()) diff --git a/app/src/main/java/com/futo/circles/extensions/MatrixRoomExtensions.kt b/app/src/main/java/com/futo/circles/extensions/MatrixRoomExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..d0797744aa234b43d028dbcc3b54e62046f10ced --- /dev/null +++ b/app/src/main/java/com/futo/circles/extensions/MatrixRoomExtensions.kt @@ -0,0 +1,9 @@ +package com.futo.circles.extensions + +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 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 new file mode 100644 index 0000000000000000000000000000000000000000..cb01b978ce9b9c6f0a98d5574bb157c557f7b007 --- /dev/null +++ b/app/src/main/java/com/futo/circles/extensions/RecyclerViewExtensions.kt @@ -0,0 +1,8 @@ +package com.futo.circles.extensions + + +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +fun RecyclerView.ViewHolder.onClick(view: View, perform: (adapterPosition: Int) -> Unit) = + view.setOnClickListener { bindingAdapterPosition.takeIf { it != -1 }?.let(perform) } \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/ui/bottom_navigation/BottomNavigationFragment.kt b/app/src/main/java/com/futo/circles/ui/bottom_navigation/BottomNavigationFragment.kt index 37657ec11bf82b9ef29dcff8173e485e542ed439..5a3a49d6dd5b70e1974a4e2ec80e39f04fb45e4f 100644 --- a/app/src/main/java/com/futo/circles/ui/bottom_navigation/BottomNavigationFragment.kt +++ b/app/src/main/java/com/futo/circles/ui/bottom_navigation/BottomNavigationFragment.kt @@ -10,10 +10,11 @@ import androidx.navigation.ui.setupWithNavController import by.kirich1409.viewbindingdelegate.viewBinding import com.futo.circles.R import com.futo.circles.databinding.BottomNavigationFragmentBinding +import com.futo.circles.extensions.setSupportActionBar class BottomNavigationFragment : Fragment(R.layout.bottom_navigation_fragment) { - private val binding: BottomNavigationFragmentBinding by viewBinding() + private val binding by viewBinding(BottomNavigationFragmentBinding::bind) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -22,13 +23,14 @@ class BottomNavigationFragment : Fragment(R.layout.bottom_navigation_fragment) { binding.bottomNavigationView.setupWithNavController(controller) setupToolBar(controller) } - } private fun findChildNavController() = (childFragmentManager.findFragmentById(R.id.bottom_nav_host_fragment) as? NavHostFragment)?.navController private fun setupToolBar(navController: NavController) { + setSupportActionBar(binding.toolbar) + val appBarConfiguration = AppBarConfiguration( setOf( R.id.homeFragment, 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 609b8af17f7a12b0098f45cc5821de55e2d62639..9d60a949ad5efde4883f37264008395db9b077c7 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 @@ -1,6 +1,52 @@ package com.futo.circles.ui.groups +import android.os.Bundle +import android.view.View import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.DividerItemDecoration +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.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) \ No newline at end of file + +class GroupsFragment : Fragment(R.layout.groups_fragment) { + + private val viewModel by viewModel<GroupsViewModel>() + private val binding by viewBinding(GroupsFragmentBinding::bind) + private val listAdapter by lazy { + GroupsListAdapter( + get<MatrixSessionProvider>().currentSession?.contentUrlResolver(), + ::onGroupListItemClicked + ) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.rvGroups.apply { + addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) + adapter = listAdapter + } + + viewModel.groupsLiveData?.observeData(this, ::setGroupsList) + } + + private fun setGroupsList(list: List<RoomSummary>) { + listAdapter.submitList(list) + } + + private fun onGroupListItemClicked(room: RoomSummary) { + findNavController().navigate( + GroupsFragmentDirections.actionGroupsFragment2ToGroupTimelineFragment( + room.roomId + ) + ) + } +} \ 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 new file mode 100644 index 0000000000000000000000000000000000000000..0a0776ee6c046d8dec9001cd62a7c659cad2d325 --- /dev/null +++ b/app/src/main/java/com/futo/circles/ui/groups/GroupsViewModel.kt @@ -0,0 +1,18 @@ +package com.futo.circles.ui.groups + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.map +import com.futo.circles.extensions.containsTag +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 +) : ViewModel() { + + val groupsLiveData = + matrixSessionProvider.currentSession?.getRoomSummariesLive(roomSummaryQueryParams()) + ?.map { list -> list.containsTag(GROUP_TAG) } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..57bd8dac82e94de52c5ebd93ac9b9d9fe796fbaf --- /dev/null +++ b/app/src/main/java/com/futo/circles/ui/groups/list/GroupViewHolder.kt @@ -0,0 +1,58 @@ +package com.futo.circles.ui.groups.list + +import android.text.format.DateUtils +import android.view.ViewGroup +import com.futo.circles.R +import com.futo.circles.base.BaseRecyclerViewHolder +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 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 +) { + + init { + onClick(itemView) { position -> onGroupClicked(position) } + } + + override fun bind(data: RoomSummary) { + with(binding) { + ivGroup.loadMatrixThumbnail(data.avatarUrl, urlResolver) + + ivLock.setImageResource(if (data.isEncrypted) R.drawable.ic_lock else R.drawable.ic_lock_open) + + tvGroupTitle.text = data.nameOrId() + + val membersCount = data.joinedMembersCount ?: 0 + tvMembers.text = context.resources.getQuantityString( + R.plurals.member_plurals, + membersCount, 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 + ) + ) + } + } + } +} \ 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 new file mode 100644 index 0000000000000000000000000000000000000000..6395ce4e8a110071824f0d29087d0a4bd387efc4 --- /dev/null +++ b/app/src/main/java/com/futo/circles/ui/groups/list/GroupsListAdapter.kt @@ -0,0 +1,26 @@ +package com.futo.circles.ui.groups.list + +import android.view.ViewGroup +import com.futo.circles.base.BaseRvAdapter +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()) { + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): GroupViewHolder = GroupViewHolder( + parent = parent, + urlResolver = urlResolver, + onGroupClicked = { position -> getItem(position)?.let { onGroupClicked(it) } } + ) + + override fun onBindViewHolder(holder: GroupViewHolder, position: Int) { + getItem(position)?.let { holder.bind(it) } + } + +} \ 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 new file mode 100644 index 0000000000000000000000000000000000000000..fa16b9618902bf788fef3f12268de069d135d201 --- /dev/null +++ b/app/src/main/java/com/futo/circles/ui/groups/timeline/GroupTimelineFragment.kt @@ -0,0 +1,26 @@ +package com.futo.circles.ui.groups.timeline + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.navArgs +import by.kirich1409.viewbindingdelegate.viewBinding +import com.futo.circles.R +import com.futo.circles.databinding.GroupTimelineFragmentBinding +import com.futo.circles.extensions.observeData +import com.futo.circles.extensions.setToolbarTitle +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.parameter.parametersOf + +class GroupTimelineFragment : Fragment(R.layout.group_timeline_fragment) { + + private val args: GroupTimelineFragmentArgs by navArgs() + private val viewModel by viewModel<GroupTimelineViewModel> { parametersOf(args.roomId) } + private val binding by viewBinding(GroupTimelineFragmentBinding::bind) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewModel.titleLiveData.observeData(this) { title -> setToolbarTitle(title) } + } +} \ 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 new file mode 100644 index 0000000000000000000000000000000000000000..de15ed4849a462090e25a6830c51ce95073c8e6e --- /dev/null +++ b/app/src/main/java/com/futo/circles/ui/groups/timeline/GroupTimelineViewModel.kt @@ -0,0 +1,17 @@ +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 + +class GroupTimelineViewModel( + private val roomId: String, + private val matrixSessionProvider: MatrixSessionProvider +) : ViewModel() { + + val titleLiveData = MutableLiveData(getRoom()?.roomSummary()?.nameOrId() ?: roomId) + + private fun getRoom() = matrixSessionProvider.currentSession?.getRoom(roomId) + +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/circles/ui/log_in/LogInFragment.kt b/app/src/main/java/com/futo/circles/ui/log_in/LogInFragment.kt index a764b509f9ceae130cb6c42399e20148608e7c6e..691fc43213e28f8f268cd551d117b3730489858d 100644 --- a/app/src/main/java/com/futo/circles/ui/log_in/LogInFragment.kt +++ b/app/src/main/java/com/futo/circles/ui/log_in/LogInFragment.kt @@ -16,7 +16,7 @@ import org.koin.androidx.viewmodel.ext.android.viewModel class LogInFragment : Fragment(R.layout.log_in_fragment) { private val viewModel by viewModel<LogInViewModel>() - private val binding: LogInFragmentBinding by viewBinding() + private val binding by viewBinding(LogInFragmentBinding::bind) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/app/src/main/java/com/futo/circles/utils/Constants.kt b/app/src/main/java/com/futo/circles/utils/Constants.kt new file mode 100644 index 0000000000000000000000000000000000000000..6c9294b5260cbbea12b99b508f784bad3f34aced --- /dev/null +++ b/app/src/main/java/com/futo/circles/utils/Constants.kt @@ -0,0 +1,3 @@ +package com.futo.circles.utils + +const val GROUP_TAG = "group" \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_circles.xml b/app/src/main/res/drawable/ic_circles.xml new file mode 100644 index 0000000000000000000000000000000000000000..620c4a5602c9b273e464775832c282996565db46 --- /dev/null +++ b/app/src/main/res/drawable/ic_circles.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M7.7543,7.3043C9.692,7.3043 11.2629,5.6692 11.2629,3.6522C11.2629,1.6351 9.692,0 7.7543,0C5.8165,0 4.2457,1.6351 4.2457,3.6522C4.2457,5.6692 5.8165,7.3043 7.7543,7.3043ZM16.2457,7.3043C18.1834,7.3043 19.7543,5.6692 19.7543,3.6522C19.7543,1.6351 18.1834,0 16.2457,0C14.308,0 12.7371,1.6351 12.7371,3.6522C12.7371,5.6692 14.308,7.3043 16.2457,7.3043ZM19.7543,20.3478C19.7543,22.3649 18.1835,24 16.2457,24C14.308,24 12.7371,22.3649 12.7371,20.3478C12.7371,18.3308 14.308,16.6957 16.2457,16.6957C18.1835,16.6957 19.7543,18.3308 19.7543,20.3478ZM20.4914,15.6522C22.4291,15.6522 24,14.017 24,12C24,9.983 22.4291,8.3478 20.4914,8.3478C18.5537,8.3478 16.9828,9.983 16.9828,12C16.9828,14.017 18.5537,15.6522 20.4914,15.6522ZM11.2629,20.3478C11.2629,22.3649 9.692,24 7.7543,24C5.8165,24 4.2457,22.3649 4.2457,20.3478C4.2457,18.3308 5.8165,16.6957 7.7543,16.6957C9.692,16.6957 11.2629,18.3308 11.2629,20.3478ZM3.5086,15.6522C5.4464,15.6522 7.0172,14.017 7.0172,12C7.0172,9.983 5.4464,8.3478 3.5086,8.3478C1.5709,8.3478 0,9.983 0,12C0,14.017 1.5709,15.6522 3.5086,15.6522Z" + android:fillColor="#ffffff" + android:fillType="evenOdd"/> +</vector> diff --git a/app/src/main/res/drawable/ic_lock.xml b/app/src/main/res/drawable/ic_lock.xml new file mode 100644 index 0000000000000000000000000000000000000000..0fc91867b489a29055712aeba3686e02e260038d --- /dev/null +++ b/app/src/main/res/drawable/ic_lock.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="@color/blue" + android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z" /> +</vector> diff --git a/app/src/main/res/drawable/ic_lock_open.xml b/app/src/main/res/drawable/ic_lock_open.xml new file mode 100644 index 0000000000000000000000000000000000000000..845c2e4b1333f1d2144621405930a769f7688ad4 --- /dev/null +++ b/app/src/main/res/drawable/ic_lock_open.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="@color/red" + android:pathData="M12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6h1.9c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM18,20L6,20L6,10h12v10z" /> +</vector> diff --git a/app/src/main/res/layout/bottom_navigation_fragment.xml b/app/src/main/res/layout/bottom_navigation_fragment.xml index 4397c00b0e28da4de2825f694f20a81d1aebd0b9..6d1f9e5a8cc62e1b367bcf093cf70526d7b649e9 100644 --- a/app/src/main/res/layout/bottom_navigation_fragment.xml +++ b/app/src/main/res/layout/bottom_navigation_fragment.xml @@ -1,7 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> @@ -12,8 +11,17 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:titleCentered="true" - tools:title="Home" /> + app:title="@string/welcome" + app:titleCentered="true" /> + + <View + android:id="@+id/toolbarDivider" + android:layout_width="0dp" + android:layout_height="@dimen/divider_height" + android:background="@color/divider_color" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/toolbar" /> <androidx.fragment.app.FragmentContainerView android:id="@+id/bottom_nav_host_fragment" @@ -21,13 +29,21 @@ android:layout_width="0dp" android:layout_height="0dp" app:defaultNavHost="true" - - app:layout_constraintBottom_toTopOf="@id/bottomNavigationView" + app:layout_constraintBottom_toTopOf="@id/bottomMenuDivider" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" - app:layout_constraintTop_toBottomOf="@id/toolbar" + app:layout_constraintTop_toBottomOf="@id/toolbarDivider" app:navGraph="@navigation/nav_graph_bottom_menu" /> + <View + android:id="@+id/bottomMenuDivider" + android:layout_width="0dp" + android:layout_height="@dimen/divider_height" + android:background="@color/divider_color" + app:layout_constraintBottom_toTopOf="@id/bottomNavigationView" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> + <com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/bottomNavigationView" android:layout_width="0dp" diff --git a/app/src/main/res/layout/group_list_item.xml b/app/src/main/res/layout/group_list_item.xml new file mode 100644 index 0000000000000000000000000000000000000000..29c512f8f086916c0fda2ab233c23d45b97375e5 --- /dev/null +++ b/app/src/main/res/layout/group_list_item.xml @@ -0,0 +1,95 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?selectableItemBackground" + android:clickable="true" + android:focusable="true" + android:padding="4dp"> + + + <androidx.cardview.widget.CardView + android:id="@+id/card" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:cardCornerRadius="10dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + <ImageView + android:id="@+id/ivGroup" + android:layout_width="120dp" + android:layout_height="120dp" + android:scaleType="fitCenter" /> + + </androidx.cardview.widget.CardView> + + + <ImageView + android:id="@+id/ivLock" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginStart="8dp" + android:src="@drawable/ic_lock" + app:layout_constraintBottom_toBottomOf="@id/tvGroupTitle" + app:layout_constraintDimensionRatio="h,1:1" + app:layout_constraintStart_toEndOf="@id/card" + app:layout_constraintTop_toTopOf="@id/tvGroupTitle" /> + + <TextView + android:id="@+id/tvGroupTitle" + style="@style/title2" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginTop="4dp" + android:ellipsize="end" + android:lines="1" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/ivLock" + app:layout_constraintTop_toTopOf="parent" + tools:text="texsdt" /> + + <TextView + android:id="@+id/tvMembers" + style="@style/subheadline" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:ellipsize="end" + android:lines="1" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@id/ivLock" + app:layout_constraintTop_toBottomOf="@id/tvGroupTitle" + tools:text="texsdt" /> + + <TextView + android:id="@+id/tvTopic" + style="@style/subheadline" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:ellipsize="end" + android:lines="1" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@id/ivLock" + app:layout_constraintTop_toBottomOf="@id/tvMembers" + tools:text="texsdt" /> + + <TextView + android:id="@+id/tvUpdateTime" + style="@style/subheadline" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:ellipsize="end" + android:lines="1" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@id/ivLock" + app:layout_constraintTop_toBottomOf="@id/tvTopic" + tools:text="texsdt" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ 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 new file mode 100644 index 0000000000000000000000000000000000000000..77d9ef65f8c7c6bf54bdef7d54d1646b86417404 --- /dev/null +++ b/app/src/main/res/layout/group_timeline_fragment.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/groups_fragment.xml b/app/src/main/res/layout/groups_fragment.xml index 77d9ef65f8c7c6bf54bdef7d54d1646b86417404..9c38fa4287e5232b558b74335066d3066635309c 100644 --- a/app/src/main/res/layout/groups_fragment.xml +++ b/app/src/main/res/layout/groups_fragment.xml @@ -1,6 +1,13 @@ <?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" 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/rvGroups" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> + +</FrameLayout> \ No newline at end of file diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml index 4c93efda76a19f1adf32f848e8def392464f7ece..3af12515ae0604a3ba88c33e248859ca7d9dfc4e 100644 --- a/app/src/main/res/menu/bottom_nav_menu.xml +++ b/app/src/main/res/menu/bottom_nav_menu.xml @@ -8,7 +8,7 @@ <item android:id="@+id/circlesFragment" - android:icon="@drawable/ic_keyboard_arrow_right" + android:icon="@drawable/ic_circles" android:title="@string/circles" /> <item @@ -17,7 +17,7 @@ android:title="@string/people" /> <item - android:id="@+id/groupsFragment" + android:id="@+id/groups_nav_graph" android:icon="@drawable/ic_round_people" android:title="@string/groups" /> diff --git a/app/src/main/res/navigation/groups_nav_graph.xml b/app/src/main/res/navigation/groups_nav_graph.xml new file mode 100644 index 0000000000000000000000000000000000000000..6724ae83cf81224c66dac054fbfb1f5d8f5294a9 --- /dev/null +++ b/app/src/main/res/navigation/groups_nav_graph.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<navigation xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/groups_nav_graph" + app:startDestination="@id/groupsFragment"> + + <fragment + android:id="@+id/groupsFragment" + android:name="com.futo.circles.ui.groups.GroupsFragment" + android:label="@string/my_groups" + tools:layout="@layout/groups_fragment"> + <action + android:id="@+id/action_groupsFragment2_to_groupTimelineFragment" + app:destination="@id/groupTimelineFragment"> + + <argument + android:name="roomId" + app:argType="string" + app:nullable="false" /> + + </action> + </fragment> + <fragment + android:id="@+id/groupTimelineFragment" + android:name="com.futo.circles.ui.groups.timeline.GroupTimelineFragment" + tools:layout="@layout/group_timeline_fragment"> + + <argument + android:name="roomId" + app:argType="string" + app:nullable="false" /> + + </fragment> +</navigation> \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph_bottom_menu.xml b/app/src/main/res/navigation/nav_graph_bottom_menu.xml index 8b0eeed06d37e078293c666e3fd81894bc94e5ee..5e7ce4402d51c6833a0a15e6c95115fe5b157616 100644 --- a/app/src/main/res/navigation/nav_graph_bottom_menu.xml +++ b/app/src/main/res/navigation/nav_graph_bottom_menu.xml @@ -8,27 +8,25 @@ <fragment android:id="@+id/homeFragment" android:name="com.futo.circles.ui.home.HomeFragment" - android:label="@string/home" + android:label="@string/welcome" tools:layout="@layout/home_fragment" /> <fragment android:id="@+id/circlesFragment" android:name="com.futo.circles.ui.circles.CirclesFragment" - android:label="@string/circles" + android:label="@string/my_circles" tools:layout="@layout/circles_fragment" /> - <fragment - android:id="@+id/groupsFragment" - android:name="com.futo.circles.ui.groups.GroupsFragment" - android:label="@string/groups" - tools:layout="@layout/groups_fragment" /> + + <include app:graph="@navigation/groups_nav_graph" /> + <fragment android:id="@+id/peopleFragment" android:name="com.futo.circles.ui.people.PeopleFragment" - android:label="@string/people" + android:label="@string/my_people" tools:layout="@layout/people_fragment" /> <fragment android:id="@+id/photosFragment" android:name="com.futo.circles.ui.photos.PhotosFragment" - android:label="@string/photos" + android:label="@string/photo_galleries" tools:layout="@layout/photos_fragment" /> </navigation> \ 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 a6649b036ce014d8362b01ec047aa3dcd5ed92e5..aa48f84f6246b1f70549b5c7d2a3f5d70488aef4 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -17,5 +17,7 @@ <color name="orange">#FF9500</color> <color name="red">#FF3B30</color> <color name="pink">#FF2D55</color> + <color name="divider_color">#ddd</color> + <color name="gray">#8E8E93</color> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000000000000000000000000000000000000..e6d1263553903a59150ae19519a5b8552d8d880c --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <dimen name="divider_height">1dp</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 30f3d0a91408c24cb30cb1e803ec9c6a48cb37a0..b13859d89244b3b1c4853b46f44fd7871eeebe75 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,4 +13,17 @@ <string name="people">People</string> <string name="photos">Photos</string> <string name="home">Home</string> + <string name="topic_formatter">Topic: %s</string> + <string name="last_updated_formatter">Last updated %s</string> + <string name="none">none</string> + <string name="my_groups">My Groups</string> + <string name="welcome">Welcome!</string> + <string name="my_people">My People</string> + <string name="my_circles">My Circles</string> + <string name="photo_galleries">Photo Galleries</string> + + <plurals name="member_plurals"> + <item quantity="one">%d member</item> + <item quantity="other">%d members</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 f9269a3e3488af3495dc06aa685b1a63ec57404a..f2feacee049c94f0227eb8099d580bc5b872e5d7 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -18,6 +18,11 @@ <item name="android:textSize">34sp</item> </style> + <style name="title2" parent="TextAppearance.MaterialComponents.Headline6"> + <item name="android:textStyle">bold</item> + <item name="android:textSize">22sp</item> + </style> + <style name="body" parent="TextAppearance.MaterialComponents.Headline6"> <item name="android:textStyle">normal</item> <item name="android:textSize">17sp</item> @@ -27,4 +32,10 @@ <item name="android:textStyle">normal</item> <item name="android:textSize">13sp</item> </style> + + <style name="subheadline" parent="TextAppearance.MaterialComponents.Headline6"> + <item name="android:textStyle">normal</item> + <item name="android:textColor">@color/gray</item> + <item name="android:textSize">15sp</item> + </style> </resources> \ No newline at end of file