This commit is contained in:
erik_everardo 2023-07-15 20:58:57 -06:00
parent 1807d6145f
commit 19b61c959b
62 changed files with 1240 additions and 351 deletions

View File

@ -18,7 +18,7 @@
<PersistentState> <PersistentState>
<option name="values"> <option name="values">
<map> <map>
<entry key="url" value="file:/$USER_HOME$/AppData/Local/Android/Sdk/icons/material/materialicons/search/baseline_search_24.xml" /> <entry key="url" value="file:$USER_HOME$/Android/Sdk/icons/material/materialicons/mic/baseline_mic_24.xml" />
</map> </map>
</option> </option>
</PersistentState> </PersistentState>
@ -28,8 +28,8 @@
</option> </option>
<option name="values"> <option name="values">
<map> <map>
<entry key="outputName" value="baseline_search_24" /> <entry key="outputName" value="baseline_mic_24" />
<entry key="sourceFile" value="C:\Users\erike\Downloads\comments-solid.svg" /> <entry key="sourceFile" value="$USER_HOME$/C:\Users\erike\Downloads\comments-solid.svg" />
</map> </map>
</option> </option>
</PersistentState> </PersistentState>

6
.idea/kotlinc.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.8.0" />
</component>
</project>

242
.idea/navEditor.xml generated Normal file
View File

@ -0,0 +1,242 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="navEditor-manualLayoutAlgorithm2">
<option name="myPositions">
<map>
<entry key="audio_recorder_navigation.xml">
<value>
<LayoutPositions>
<option name="myPositions">
<map>
<entry key="audioDraftsFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="130" />
<option name="y" value="18" />
</Point>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="audioRecorderMainFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-74" />
<option name="y" value="17" />
</Point>
</option>
</LayoutPositions>
</value>
</entry>
</map>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="home_navigation.xml">
<value>
<LayoutPositions>
<option name="myPositions">
<map>
<entry key="feedFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-280" />
<option name="y" value="-77" />
</Point>
</option>
<option name="myPositions">
<map>
<entry key="action_feedFragment_to_profileActivity">
<value>
<LayoutPositions />
</value>
</entry>
</map>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="notificationsFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-282" />
<option name="y" value="-368" />
</Point>
</option>
<option name="myPositions">
<map>
<entry key="action_notificationsFragment_to_profileActivity">
<value>
<LayoutPositions />
</value>
</entry>
</map>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="profileActivity">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="31" />
<option name="y" value="-367" />
</Point>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="searchFragment2">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="-281" />
<option name="y" value="221" />
</Point>
</option>
</LayoutPositions>
</value>
</entry>
</map>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="profile_navigation.xml">
<value>
<LayoutPositions>
<option name="myPositions">
<map>
<entry key="audiosFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="256" />
<option name="y" value="12" />
</Point>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="discussionsFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="12" />
<option name="y" value="12" />
</Point>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="imagesFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="12" />
<option name="y" value="368" />
</Point>
</option>
</LayoutPositions>
</value>
</entry>
</map>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="settings_navigation.xml">
<value>
<LayoutPositions>
<option name="myPositions">
<map>
<entry key="changePasswordFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="256" />
<option name="y" value="368" />
</Point>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="feedSettingsFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="256" />
<option name="y" value="12" />
</Point>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="sessionsFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="256" />
<option name="y" value="724" />
</Point>
</option>
</LayoutPositions>
</value>
</entry>
<entry key="settingsFragment">
<value>
<LayoutPositions>
<option name="myPosition">
<Point>
<option name="x" value="12" />
<option name="y" value="76" />
</Point>
</option>
<option name="myPositions">
<map>
<entry key="action_settingsFragment_to_changePasswordFragment">
<value>
<LayoutPositions />
</value>
</entry>
<entry key="action_settingsFragment_to_feedSettingsFragment">
<value>
<LayoutPositions />
</value>
</entry>
<entry key="action_settingsFragment_to_sessionsFragment">
<value>
<LayoutPositions />
</value>
</entry>
</map>
</option>
</LayoutPositions>
</value>
</entry>
</map>
</option>
</LayoutPositions>
</value>
</entry>
</map>
</option>
</component>
</project>

View File

@ -4,6 +4,7 @@ plugins {
id 'kotlin-kapt' id 'kotlin-kapt'
id 'com.google.dagger.hilt.android' id 'com.google.dagger.hilt.android'
id 'org.jetbrains.kotlin.plugin.serialization' version '1.8.0' id 'org.jetbrains.kotlin.plugin.serialization' version '1.8.0'
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
} }
android { android {
@ -89,6 +90,19 @@ dependencies {
implementation "io.noties.markwon:editor:$markwon_version" implementation "io.noties.markwon:editor:$markwon_version"
implementation "io.noties.markwon:image-picasso:$markwon_version" implementation "io.noties.markwon:image-picasso:$markwon_version"
implementation "io.noties.markwon:linkify:$markwon_version" implementation "io.noties.markwon:linkify:$markwon_version"
implementation ('io.socket:socket.io-client:2.1.0') {
// excluding org.json which is provided by Android
exclude group: 'org.json', module: 'json'
}
def room_version = "2.5.2"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
} }
kapt { kapt {

View File

@ -11,11 +11,12 @@
android:label="@string/app_name" android:label="@string/app_name"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Isolaatti" android:theme="@style/Theme.Isolaatti"
android:usesCleartextTraffic="true"
tools:targetApi="31"> tools:targetApi="31">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:theme="@style/Theme.Isolaatti.Splash" android:noHistory="true"> android:theme="@style/Theme.Isolaatti.Splash">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@ -0,0 +1,43 @@
package com.isolaatti
import android.app.Activity
import android.app.Application
import android.os.Bundle
import com.isolaatti.connectivity.SocketIO
class ActivityLifecycleCallbacks : Application.ActivityLifecycleCallbacks {
var startedActivitiesCount = 0
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {
startedActivitiesCount++
}
override fun onActivityStarted(activity: Activity) {
}
override fun onActivityResumed(activity: Activity) {
}
override fun onActivityPaused(activity: Activity) {
}
override fun onActivityStopped(activity: Activity) {
}
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {
}
override fun onActivityDestroyed(activity: Activity) {
startedActivitiesCount--
if(startedActivitiesCount == 0) {
SocketIO.disconnect()
}
}
}

View File

@ -1,8 +1,10 @@
package com.isolaatti package com.isolaatti
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import com.isolaatti.auth.data.AuthRepositoryImpl import com.isolaatti.auth.data.AuthRepositoryImpl
import com.isolaatti.home.HomeActivity import com.isolaatti.home.HomeActivity
@ -15,6 +17,14 @@ class MainActivity : ComponentActivity() {
@Inject @Inject
lateinit var authRepository: AuthRepositoryImpl lateinit var authRepository: AuthRepositoryImpl
private val signInActivityResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
if(activityResult.resultCode == Activity.RESULT_OK) {
val homeActivityIntent = Intent(this@MainActivity, HomeActivity::class.java)
homeActivityIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(homeActivityIntent)
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
var isLoading = true var isLoading = true
val splashScreen = installSplashScreen() val splashScreen = installSplashScreen()
@ -25,11 +35,12 @@ class MainActivity : ComponentActivity() {
val currentToken = authRepository.getCurrentToken() val currentToken = authRepository.getCurrentToken()
if(currentToken == null) { if(currentToken == null) {
startActivity(Intent(this@MainActivity, LogInActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NO_HISTORY signInActivityResult.launch(Intent(this@MainActivity, LogInActivity::class.java))
})
} else { } else {
startActivity(Intent(this@MainActivity, HomeActivity::class.java)) val homeActivityIntent = Intent(this@MainActivity, HomeActivity::class.java)
homeActivityIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(homeActivityIntent)
} }
isLoading = false isLoading = false
} }

View File

@ -1,6 +1,8 @@
package com.isolaatti package com.isolaatti
import android.app.Activity
import android.app.Application import android.app.Application
import android.os.Bundle
import com.isolaatti.auth.data.AuthRepositoryImpl import com.isolaatti.auth.data.AuthRepositoryImpl
import com.isolaatti.auth.data.local.TokenStorage import com.isolaatti.auth.data.local.TokenStorage
import com.isolaatti.auth.data.remote.AuthApi import com.isolaatti.auth.data.remote.AuthApi
@ -13,7 +15,15 @@ import javax.inject.Singleton
@HiltAndroidApp @HiltAndroidApp
class MyApplication : Application() { class MyApplication : Application() {
private val activityLifecycleCallbacks = ActivityLifecycleCallbacks()
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
registerActivityLifecycleCallbacks(activityLifecycleCallbacks)
}
override fun onTerminate() {
super.onTerminate()
unregisterActivityLifecycleCallbacks(activityLifecycleCallbacks)
} }
} }

View File

@ -0,0 +1,6 @@
package com.isolaatti.audio.recorder.ui
import androidx.fragment.app.Fragment
class AudioDraftsFragment : Fragment() {
}

View File

@ -0,0 +1,6 @@
package com.isolaatti.audio.recorder.ui
import androidx.activity.ComponentActivity
class AudioRecorderActivity : ComponentActivity() {
}

View File

@ -0,0 +1,6 @@
package com.isolaatti.audio.recorder.ui
import androidx.fragment.app.Fragment
class AudioRecorderMainFragment : Fragment() {
}

View File

@ -1,18 +1,16 @@
package com.isolaatti.auth.data package com.isolaatti.auth.data
import android.util.Log
import com.isolaatti.auth.data.remote.AuthTokenDto import com.isolaatti.auth.data.remote.AuthTokenDto
import com.isolaatti.auth.data.local.TokenStorage import com.isolaatti.auth.data.local.TokenStorage
import com.isolaatti.auth.data.remote.AuthApi import com.isolaatti.auth.data.remote.AuthApi
import com.isolaatti.auth.data.remote.Credential import com.isolaatti.auth.data.remote.Credential
import com.isolaatti.auth.domain.AuthRepository import com.isolaatti.auth.domain.AuthRepository
import kotlinx.coroutines.CoroutineScope import com.isolaatti.utils.Resource
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch import retrofit2.await
import retrofit2.awaitResponse import retrofit2.awaitResponse
import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
@ -20,22 +18,41 @@ class AuthRepositoryImpl @Inject constructor(
private val tokenStorage: TokenStorage, private val tokenStorage: TokenStorage,
private val authApi: AuthApi private val authApi: AuthApi
) : AuthRepository { ) : AuthRepository {
override fun authWithEmailAndPassword(email: String, password: String): Flow<Boolean> = flow { override fun authWithEmailAndPassword(
val res = authApi.signInWithEmailAndPassword(Credential(email, password)).awaitResponse() email: String,
val authDto = res.body() password: String
): Flow<Resource<Boolean>> = flow {
try {
val res =
authApi.signInWithEmailAndPassword(Credential(email, password)).awaitResponse()
if(res.isSuccessful) {
val dto = res.body()
if(dto == null) {
emit(Resource.Error(Resource.Error.ErrorType.ServerError))
return@flow
}
tokenStorage.storeToken(dto)
emit(Resource.Success(true))
return@flow
}
if(authDto != null) { when(res.code()){
tokenStorage.storeToken(authDto) 401 -> emit(Resource.Error(Resource.Error.ErrorType.AuthError))
emit(true) 404 -> emit(Resource.Error(Resource.Error.ErrorType.NotFoundError))
} else { 500 -> emit(Resource.Error(Resource.Error.ErrorType.ServerError))
emit(false) else -> emit(Resource.Error(Resource.Error.ErrorType.OtherError))
}
} catch (_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
} }
} }
override fun logout(): Flow<Boolean> = flow { override fun logout(): Flow<Boolean> = flow {
tokenStorage.removeToken() tokenStorage.removeToken()
authApi.signOut() try {
authApi.signOut().awaitResponse()
} catch(_: Exception) { }
emit(true) emit(true)
} }

View File

@ -79,7 +79,9 @@ class TokenStorage @Inject constructor(@ApplicationContext private val applicati
fun removeToken() { fun removeToken() {
if(file.exists()) { if(file.exists()) {
file.delete() try {
file.delete()
} catch(_: SecurityException) { }
} }
} }
} }

View File

@ -1,6 +1,6 @@
package com.isolaatti.auth.data.remote package com.isolaatti.auth.data.remote
data class AuthTokenDto(val expires: String, val created: String, val token: String) { data class AuthTokenDto(val token: String) {
override fun toString(): String { override fun toString(): String {
return token return token
} }

View File

@ -1,10 +1,11 @@
package com.isolaatti.auth.domain package com.isolaatti.auth.domain
import com.isolaatti.auth.data.remote.AuthTokenDto import com.isolaatti.auth.data.remote.AuthTokenDto
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface AuthRepository { interface AuthRepository {
fun authWithEmailAndPassword(email: String, password: String): Flow<Boolean> fun authWithEmailAndPassword(email: String, password: String): Flow<Resource<Boolean>>
fun logout(): Flow<Boolean> fun logout(): Flow<Boolean>
fun getCurrentToken(): AuthTokenDto? fun getCurrentToken(): AuthTokenDto?
} }

View File

@ -0,0 +1,9 @@
package com.isolaatti.common
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.isolaatti.utils.Resource
class ErrorMessageViewModel : ViewModel() {
val error: MutableLiveData<Resource.Error.ErrorType> = MutableLiveData()
}

View File

@ -0,0 +1,81 @@
package com.isolaatti.common
import android.app.Activity
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.isolaatti.R
import com.isolaatti.home.HomeActivity
import com.isolaatti.utils.Resource
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
abstract class IsolaattiBaseActivity : AppCompatActivity() {
val errorViewModel: ErrorMessageViewModel by viewModels()
private val errorObserver: Observer<Resource.Error.ErrorType> = Observer {
when(it) {
Resource.Error.ErrorType.AuthError -> showReAuthDialog()
Resource.Error.ErrorType.NetworkError -> showNetworkErrorMessage()
Resource.Error.ErrorType.NotFoundError -> showNotFoundErrorMessage()
Resource.Error.ErrorType.ServerError -> showServerErrorMessage()
Resource.Error.ErrorType.OtherError -> showUnknownErrorMessage()
else -> {}
}
}
private val signInActivityResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult ->
if(activityResult.resultCode == Activity.RESULT_OK) {
onRetry()
}
}
/**
* This method is called when a refresh should be performed. For example,
* when sign in flow is started completed from here, it is needed to know
* when it is complete.
*/
abstract fun onRetry()
private val onAcceptReAuthClick = DialogInterface.OnClickListener { _, _ ->
}
private fun showReAuthDialog() {
MaterialAlertDialogBuilder(this)
.setMessage(R.string.need_reauth_message)
.setPositiveButton(R.string.accept, onAcceptReAuthClick)
.setNegativeButton(R.string.close, null)
.show()
}
private fun showNetworkErrorMessage() {
Toast.makeText(this, R.string.network_error, Toast.LENGTH_SHORT).show()
}
private fun showServerErrorMessage() {
}
private fun showNotFoundErrorMessage() {
}
private fun showUnknownErrorMessage() {
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
errorViewModel.error.observe(this, errorObserver)
Log.d("IsolaattiBaseActivity", errorViewModel.toString())
}
}

View File

@ -1,5 +1,6 @@
package com.isolaatti.connectivity package com.isolaatti.connectivity
import com.isolaatti.BuildConfig
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
@ -14,7 +15,7 @@ class RetrofitClient @Inject constructor(private val authenticationInterceptor:
val excludedUrlsFromAuthentication = listOf( val excludedUrlsFromAuthentication = listOf(
"/api/LogIn" "/api/LogIn"
) )
const val BASE_URL = "https://isolaatti.com/api/" const val BASE_URL = "${BuildConfig.backend}/api/"
} }
private val okHttpClient get() = OkHttpClient.Builder() private val okHttpClient get() = OkHttpClient.Builder()

View File

@ -0,0 +1,26 @@
package com.isolaatti.connectivity
import io.socket.client.IO
import io.socket.client.Socket
object SocketIO {
private lateinit var socket: Socket
val instance: Socket get() {
return if(this::socket.isInitialized) {
if(socket.connected()) {
socket
} else {
IO.socket("")
}
} else {
socket = IO.socket("")
socket
}
}
fun disconnect() {
if(this::socket.isInitialized) {
socket.disconnect()
}
}
}

View File

@ -1,10 +0,0 @@
package com.isolaatti.feed.data.remote
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Query
interface FeedApi {
@GET("Feed")
fun fetchFeed(@Query("lastId") lastId: Long, @Query("length") length: Int): Call<FeedDto>
}

View File

@ -1,6 +0,0 @@
package com.isolaatti.feed.data.remote
data class FeedDto(
val data: MutableList<PostDto>,
val moreContent: Boolean
)

View File

@ -1,12 +0,0 @@
package com.isolaatti.feed.data.remote
import com.isolaatti.feed.domain.model.Post
data class PostDto(
val post: Post,
var numberOfLikes: Int,
var numberOfComments: Int,
val userName: String,
val squadName: String?,
var liked: Boolean
)

View File

@ -1,20 +0,0 @@
package com.isolaatti.feed.data.repository
import com.isolaatti.feed.data.remote.FeedApi
import com.isolaatti.feed.data.remote.FeedDto
import com.isolaatti.feed.domain.repository.FeedRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import retrofit2.awaitResponse
import javax.inject.Inject
class FeedRepositoryImpl @Inject constructor(private val feedApi: FeedApi): FeedRepository {
override fun getNextPage(lastId: Long, count: Int): Flow<FeedDto?> = flow {
val response = feedApi.fetchFeed(lastId, count).awaitResponse().body()
if(response != null){
emit(response)
} else {
emit(null)
}
}
}

View File

@ -1,13 +0,0 @@
package com.isolaatti.feed.domain.model
data class Post(
val id: Long,
var textContent: String,
val userId: Int,
val privacy: Int,
val date: String,
var audioId: String,
val squadId: String,
val linkedDiscussionId: Long,
val linkedCommentId: Long
)

View File

@ -1,8 +0,0 @@
package com.isolaatti.feed.domain.repository
import com.isolaatti.feed.data.remote.FeedDto
import kotlinx.coroutines.flow.Flow
interface FeedRepository {
fun getNextPage(lastId: Long, count: Int): Flow<FeedDto?>
}

View File

@ -1,4 +1,4 @@
package com.isolaatti.feed.ui package com.isolaatti.home
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
@ -10,6 +10,7 @@ import android.view.ViewGroup
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.common.ErrorMessageViewModel
import com.isolaatti.databinding.FragmentFeedBinding import com.isolaatti.databinding.FragmentFeedBinding
import com.isolaatti.posting.posts.presentation.PostsViewModel import com.isolaatti.posting.posts.presentation.PostsViewModel
import com.isolaatti.posting.comments.presentation.BottomSheetPostComments import com.isolaatti.posting.comments.presentation.BottomSheetPostComments
@ -34,6 +35,7 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
} }
private val viewModel: PostsViewModel by activityViewModels() private val viewModel: PostsViewModel by activityViewModels()
private val errorViewModel: ErrorMessageViewModel by activityViewModels()
private lateinit var viewBinding: FragmentFeedBinding private lateinit var viewBinding: FragmentFeedBinding
private lateinit var adapter: PostsRecyclerViewAdapter private lateinit var adapter: PostsRecyclerViewAdapter
@ -67,6 +69,7 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
} }
} }
val markwon = Markwon.builder(requireContext()) val markwon = Markwon.builder(requireContext())
.usePlugin(object: AbstractMarkwonPlugin() { .usePlugin(object: AbstractMarkwonPlugin() {
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) { override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
@ -78,21 +81,33 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
.usePlugin(PicassoImagesPluginDef.picassoImagePlugin) .usePlugin(PicassoImagesPluginDef.picassoImagePlugin)
.usePlugin(LinkifyPlugin.create()) .usePlugin(LinkifyPlugin.create())
.build() .build()
adapter = PostsRecyclerViewAdapter(markwon, this, listOf()) adapter = PostsRecyclerViewAdapter(markwon, this, null)
viewBinding.feedRecyclerView.adapter = adapter viewBinding.feedRecyclerView.adapter = adapter
viewBinding.feedRecyclerView.layoutManager = LinearLayoutManager(requireContext()) viewBinding.feedRecyclerView.layoutManager = LinearLayoutManager(requireContext())
viewModel.posts.observe(viewLifecycleOwner){ viewModel.posts.observe(viewLifecycleOwner){
Log.d("recycler", it.data.toString()) Log.d("recycler", it.data.toString())
adapter.updateList(it.data.toList(),null) adapter.updateList(it,null)
}
Log.d("FeedFragment", errorViewModel.toString())
viewModel.errorLoading.observe(viewLifecycleOwner) {
errorViewModel.error.postValue(it)
Log.d("FeedFragment", it.toString())
} }
viewModel.postLiked.observe(viewLifecycleOwner) { viewModel.postLiked.observe(viewLifecycleOwner) {
adapter.updateList(viewModel.posts.value?.data, PostsRecyclerViewAdapter.UpdateEvent( viewModel.posts.value?.let { feed ->
PostsRecyclerViewAdapter.UpdateEvent.UpdateType.POST_LIKED, it.postId)) adapter.updateList(
feed, PostsRecyclerViewAdapter.UpdateEvent(
PostsRecyclerViewAdapter.UpdateEvent.UpdateType.POST_LIKED, it.postId))
}
} }
} }
fun onNewMenuItemClicked(v: View) {
}
override fun onLiked(postId: Long) = viewModel.likePost(postId) override fun onLiked(postId: Long) = viewModel.likePost(postId)
override fun onUnLiked(postId: Long) = viewModel.unLikePost(postId) override fun onUnLiked(postId: Long) = viewModel.unLikePost(postId)

View File

@ -4,17 +4,24 @@ import android.os.Bundle
import android.view.Menu import android.view.Menu
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController import androidx.navigation.ui.setupWithNavController
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.common.ErrorMessageViewModel
import com.isolaatti.common.IsolaattiBaseActivity
import com.isolaatti.databinding.ActivityHomeBinding import com.isolaatti.databinding.ActivityHomeBinding
import com.isolaatti.posting.posts.presentation.PostsViewModel import com.isolaatti.posting.posts.presentation.PostsViewModel
import com.isolaatti.utils.Resource
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint class HomeActivity : IsolaattiBaseActivity() {
class HomeActivity : AppCompatActivity() { private lateinit var viewBinding: ActivityHomeBinding
lateinit var viewBinding: ActivityHomeBinding private val postsViewModel: PostsViewModel by viewModels()
val postsViewModel: PostsViewModel by viewModels() override fun onRetry() {
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -33,4 +40,8 @@ class HomeActivity : AppCompatActivity() {
override fun onCreateOptionsMenu(menu: Menu?): Boolean { override fun onCreateOptionsMenu(menu: Menu?): Boolean {
return super.onCreateOptionsMenu(menu) return super.onCreateOptionsMenu(menu)
} }
override fun onDestroy() {
super.onDestroy()
}
} }

View File

@ -1,27 +0,0 @@
package com.isolaatti.home
import com.isolaatti.connectivity.RetrofitClient
import com.isolaatti.feed.data.remote.FeedApi
import com.isolaatti.feed.data.repository.FeedRepositoryImpl
import com.isolaatti.feed.domain.repository.FeedRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.scopes.ActivityScoped
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class Module {
@Provides
fun provideFeedApi(retrofitClient: RetrofitClient): FeedApi {
return retrofitClient.client.create(FeedApi::class.java)
}
@Provides
fun provideFeedRepository(feedApi: FeedApi): FeedRepository {
return FeedRepositoryImpl(feedApi)
}
}

View File

@ -0,0 +1,22 @@
package com.isolaatti.home.search.presentation
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.isolaatti.databinding.FragmentSearchBinding
class SearchFragment : Fragment() {
lateinit var viewBinding: FragmentSearchBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
viewBinding = FragmentSearchBinding.inflate(inflater)
return viewBinding.root
}
}

View File

@ -1,20 +1,31 @@
package com.isolaatti.login package com.isolaatti.login
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.isolaatti.BuildConfig
import com.isolaatti.R
import com.isolaatti.common.IsolaattiBaseActivity
import com.isolaatti.databinding.ActivityLoginBinding import com.isolaatti.databinding.ActivityLoginBinding
import com.isolaatti.home.HomeActivity import com.isolaatti.home.HomeActivity
import com.isolaatti.utils.Resource
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint @AndroidEntryPoint
class LogInActivity: AppCompatActivity() { class LogInActivity: AppCompatActivity() {
lateinit var viewBinding: ActivityLoginBinding lateinit var viewBinding: ActivityLoginBinding
val viewModel: LogInViewModel by viewModels() private val viewModel: LogInViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -23,11 +34,33 @@ class LogInActivity: AppCompatActivity() {
setContentView(viewBinding.root) setContentView(viewBinding.root)
viewModel.signInSuccess.observe(this) {success -> viewModel.signInSuccess.observe(this) {success ->
if(success) if(success) {
startActivity(Intent(this, HomeActivity::class.java)) setResult(Activity.RESULT_OK)
else finish()
Toast.makeText(this,"Could not sign in, your credential is not correct...", Toast.LENGTH_SHORT).show() }
// Show login error message.
}
viewModel.signInLoading.observe(this) {
viewBinding.progressBar.visibility = View.VISIBLE
viewBinding.signInBtn.isEnabled = false
viewBinding.textFieldEmail.isEnabled = false
viewBinding.textFieldPassword.isEnabled = false
}
viewModel.signInError.observe(this) {
viewBinding.progressBar.visibility = View.GONE
viewBinding.signInBtn.isEnabled = true
viewBinding.textFieldEmail.isEnabled = true
viewBinding.textFieldPassword.isEnabled = true
when(it) {
Resource.Error.ErrorType.NetworkError -> showNetworkErrorMessage()
Resource.Error.ErrorType.AuthError -> showWrongPasswordErrorMessage()
Resource.Error.ErrorType.NotFoundError -> showNotFoundErrorMessage()
Resource.Error.ErrorType.ServerError -> showServerErrorMessage()
Resource.Error.ErrorType.OtherError -> showUnknownErrorMessage()
null -> {}
}
} }
viewModel.formIsValid.observe(this) {isValid -> viewModel.formIsValid.observe(this) {isValid ->
@ -50,5 +83,42 @@ class LogInActivity: AppCompatActivity() {
viewModel.signIn(email.toString(), password.toString()) viewModel.signIn(email.toString(), password.toString())
} }
viewBinding.forgotPasswordBtn.setOnClickListener {
openForgotPassword()
}
}
private fun openForgotPassword() {
CustomTabsIntent.Builder()
.setShowTitle(true)
.build()
.launchUrl(this, Uri.parse("${BuildConfig.backend}/recuperacion_cuenta"))
}
private fun showWrongPasswordErrorMessage() {
MaterialAlertDialogBuilder(this)
.setMessage(R.string.wrong_password)
.setNeutralButton(R.string.forgot_password) {_,_ -> openForgotPassword()}
.setPositiveButton(R.string.dismiss, null)
.show()
}
private fun showNetworkErrorMessage() {
Toast.makeText(this, R.string.network_error, Toast.LENGTH_SHORT).show()
}
private fun showServerErrorMessage() {
}
private fun showNotFoundErrorMessage() {
MaterialAlertDialogBuilder(this)
.setMessage(getString(R.string.account_not_found, viewBinding.textFieldEmail.editText?.text))
.setPositiveButton(R.string.dismiss, null)
.show()
}
private fun showUnknownErrorMessage() {
} }
} }

View File

@ -5,16 +5,22 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.isolaatti.auth.domain.AuthRepository import com.isolaatti.auth.domain.AuthRepository
import com.isolaatti.utils.Resource
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class LogInViewModel @Inject constructor(private val authRepository: AuthRepository): ViewModel() { class LogInViewModel @Inject constructor(private val authRepository: AuthRepository): ViewModel() {
val signInSuccess: MutableLiveData<Boolean> = MutableLiveData() val signInSuccess: MutableLiveData<Boolean> = MutableLiveData()
val signInError: MutableLiveData<Resource.Error.ErrorType?> = MutableLiveData()
val signInLoading: MutableLiveData<Boolean> = MutableLiveData()
val formIsValid: MutableLiveData<Boolean> = MutableLiveData(false) val formIsValid: MutableLiveData<Boolean> = MutableLiveData(false)
val emailUserInputIsValid: MutableLiveData<Boolean> = MutableLiveData(false) val emailUserInputIsValid: MutableLiveData<Boolean> = MutableLiveData(false)
val passwordUserInputIsValid: MutableLiveData<Boolean> = MutableLiveData(false) val passwordUserInputIsValid: MutableLiveData<Boolean> = MutableLiveData(false)
@ -33,11 +39,23 @@ class LogInViewModel @Inject constructor(private val authRepository: AuthReposit
} }
fun signIn(email: String, password: String) { fun signIn(email: String, password: String) {
signInError.postValue(null)
viewModelScope.launch { viewModelScope.launch {
authRepository.authWithEmailAndPassword(email, password).collect { authRepository.authWithEmailAndPassword(email, password).onEach {
Log.d("login", it.toString()) Log.d("login", it.toString())
signInSuccess.postValue(it) when(it) {
} is Resource.Success -> {
signInLoading.postValue(false)
signInSuccess.postValue(true)
}
is Resource.Error -> {
signInLoading.postValue(false)
signInError.postValue(it.errorType)
}
is Resource.Loading -> signInLoading.postValue(true)
}
}.flowOn(Dispatchers.IO).launchIn(this)
} }
} }

View File

@ -1,7 +1,7 @@
package com.isolaatti.posting package com.isolaatti.posting
import com.isolaatti.connectivity.RetrofitClient import com.isolaatti.connectivity.RetrofitClient
import com.isolaatti.posting.posts.data.remote.PostsApi import com.isolaatti.posting.posts.data.remote.FeedsApi
import com.isolaatti.posting.posts.data.repository.PostsRepositoryImpl import com.isolaatti.posting.posts.data.repository.PostsRepositoryImpl
import com.isolaatti.posting.posts.domain.PostsRepository import com.isolaatti.posting.posts.domain.PostsRepository
import dagger.Module import dagger.Module
@ -13,12 +13,12 @@ import dagger.hilt.components.SingletonComponent
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
class Module { class Module {
@Provides @Provides
fun providePostsApi(retrofitClient: RetrofitClient): PostsApi { fun providePostsApi(retrofitClient: RetrofitClient): FeedsApi {
return retrofitClient.client.create(PostsApi::class.java) return retrofitClient.client.create(FeedsApi::class.java)
} }
@Provides @Provides
fun providePostsRepository(postsApi: PostsApi): PostsRepository { fun providePostsRepository(feedsApi: FeedsApi): PostsRepository {
return PostsRepositoryImpl(postsApi) return PostsRepositoryImpl(feedsApi)
} }
} }

View File

@ -94,7 +94,7 @@ class BottomSheetPostComments() : BottomSheetDialogFragment(), OnUserInteractedC
val textField = viewBinding.newCommentTextField val textField = viewBinding.newCommentTextField
textField.setStartIconOnClickListener { textField.setStartIconOnClickListener {
AlertDialog.Builder(requireContext()).setView(R.layout.write_comment_multiline_dialog).show()
} }
optionsViewModel.optionClicked(-1) optionsViewModel.optionClicked(-1)

View File

@ -0,0 +1,36 @@
package com.isolaatti.posting.posts.data.remote
data class FeedDto(
val data: MutableList<PostDto>,
var moreContent: Boolean
) {
fun concatFeed(otherFeed: FeedDto?): FeedDto {
if(otherFeed != null) {
data.addAll(otherFeed.data)
moreContent = otherFeed.moreContent
}
return this
}
data class PostDto(
val post: Post,
var numberOfLikes: Int,
var numberOfComments: Int,
val userName: String,
val squadName: String?,
var liked: Boolean
) {
data class Post(
val id: Long,
var textContent: String,
val userId: Int,
val privacy: Int,
val date: String,
var audioId: String,
val squadId: String,
val linkedDiscussionId: Long,
val linkedCommentId: Long
)
}
}

View File

@ -0,0 +1,13 @@
package com.isolaatti.posting.posts.data.remote
data class FeedFilterDto(
val includeAudio: String,
val includeFromSquads: String,
val dataRange: DataRange
) {
data class DataRange(
val enabled: Boolean,
val from: String,
val to: String
)
}

View File

@ -1,23 +1,25 @@
package com.isolaatti.posting.posts.data.remote package com.isolaatti.posting.posts.data.remote
import com.isolaatti.feed.data.remote.FeedDto
import com.isolaatti.feed.data.remote.PostDto
import com.isolaatti.profile.data.remote.ProfileListItemDto import com.isolaatti.profile.data.remote.ProfileListItemDto
import retrofit2.Call import retrofit2.Call
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Path import retrofit2.http.Path
import retrofit2.http.Query import retrofit2.http.Query
interface PostsApi { interface FeedsApi {
@GET("Fetch/PostsOfUser/{userId}") @GET("Fetch/PostsOfUser/{userId}")
fun postsOfUser(@Path("userId") userId: Int, fun postsOfUser(@Path("userId") userId: Int,
@Query("length") length: Int, @Query("length") length: Int,
@Query("lastId") lastId: Long, @Query("lastId") lastId: Long,
@Query("olderFirst") olderFirst: Boolean): Call<FeedDto> @Query("olderFirst") olderFirst: Boolean,
@Query(value = "filterJson", encoded = false) filter: String): Call<FeedDto>
@GET("Fetch/Post/{postId}") @GET("Fetch/Post/{postId}")
fun getPost(@Path("postId") postId: Long): Call<PostDto> fun getPost(@Path("postId") postId: Long): Call<FeedDto>
@GET("Fetch/Post/{postId}/LikedBy") @GET("Fetch/Post/{postId}/LikedBy")
fun getLikedBy(@Path("postId") postId: Long): Call<List<ProfileListItemDto>> fun getLikedBy(@Path("postId") postId: Long): Call<List<ProfileListItemDto>>
@GET("Feed")
fun getChronology(@Query("lastId") lastId: Long, @Query("length") length: Int): Call<FeedDto>
} }

View File

@ -1,8 +1,58 @@
package com.isolaatti.posting.posts.data.repository package com.isolaatti.posting.posts.data.repository
import com.isolaatti.posting.posts.data.remote.PostsApi import com.google.gson.Gson
import com.isolaatti.posting.posts.data.remote.FeedDto
import com.isolaatti.posting.posts.data.remote.FeedFilterDto
import com.isolaatti.posting.posts.data.remote.FeedsApi
import com.isolaatti.posting.posts.domain.PostsRepository import com.isolaatti.posting.posts.domain.PostsRepository
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.flow
import retrofit2.await
import java.io.IOException
import java.lang.RuntimeException
import javax.inject.Inject import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class PostsRepositoryImpl @Inject constructor(private val postsApi: PostsApi) : PostsRepository { class PostsRepositoryImpl @Inject constructor(private val feedsApi: FeedsApi) : PostsRepository {
override fun getFeed(lastId: Long): Flow<Resource<FeedDto>> = flow {
emit(Resource.Loading())
try {
val result = feedsApi.getChronology(lastId, 20).execute()
if(result.isSuccessful) {
emit(Resource.Success(result.body()))
return@flow
}
when(result.code()) {
401 -> emit(Resource.Error(Resource.Error.ErrorType.AuthError))
404 -> emit(Resource.Error(Resource.Error.ErrorType.NotFoundError))
500 -> emit(Resource.Error(Resource.Error.ErrorType.ServerError))
else -> emit(Resource.Error(Resource.Error.ErrorType.OtherError))
}
} catch(_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
}
}
override fun getProfilePosts(userId: Int, lastId: Long, olderFirst: Boolean, filter: FeedFilterDto): Flow<Resource<FeedDto>> = flow {
emit(Resource.Loading())
try {
val gson = Gson()
val result = feedsApi.postsOfUser(userId, 20, lastId, olderFirst, gson.toJson(filter)).execute()
if(result.isSuccessful) {
emit(Resource.Success(result.body()))
return@flow
}
when(result.code()) {
401 -> emit(Resource.Error(Resource.Error.ErrorType.AuthError))
404 -> emit(Resource.Error(Resource.Error.ErrorType.NotFoundError))
500 -> emit(Resource.Error(Resource.Error.ErrorType.ServerError))
else -> emit(Resource.Error(Resource.Error.ErrorType.OtherError))
}
} catch(_: Exception) {
emit(Resource.Error(Resource.Error.ErrorType.NetworkError))
}
}
} }

View File

@ -1,4 +1,13 @@
package com.isolaatti.posting.posts.domain package com.isolaatti.posting.posts.domain
import com.isolaatti.posting.posts.data.remote.FeedDto
import com.isolaatti.posting.posts.data.remote.FeedFilterDto
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
interface PostsRepository { interface PostsRepository {
fun getFeed(lastId: Long): Flow<Resource<FeedDto>>
fun getProfilePosts(userId: Int, lastId: Long, olderFirst: Boolean, filter: FeedFilterDto): Flow<Resource<FeedDto>>
} }

View File

@ -11,15 +11,15 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.feed.data.remote.PostDto import com.isolaatti.posting.posts.data.remote.FeedDto
import com.isolaatti.posting.common.domain.OnUserInteractedWithPostCallback import com.isolaatti.posting.common.domain.OnUserInteractedWithPostCallback
import com.isolaatti.utils.UrlGen.userProfileImage import com.isolaatti.utils.UrlGen.userProfileImage
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
class PostsRecyclerViewAdapter (private val markwon: Markwon, private val callback: OnUserInteractedWithPostCallback, private var list: List<PostDto>) : RecyclerView.Adapter<PostsRecyclerViewAdapter.FeedViewHolder>(){ class PostsRecyclerViewAdapter (private val markwon: Markwon, private val callback: OnUserInteractedWithPostCallback, private var feedDto: FeedDto?) : RecyclerView.Adapter<PostsRecyclerViewAdapter.FeedViewHolder>(){
inner class FeedViewHolder(itemView: View) : ViewHolder(itemView) { inner class FeedViewHolder(itemView: View) : ViewHolder(itemView) {
fun bindView(postDto: PostDto, payloads: List<Any>) { fun bindView(postDto: FeedDto.PostDto, payloads: List<Any>) {
Log.d("payloads", payloads.count().toString()) Log.d("payloads", payloads.count().toString())
val likeButton: MaterialButton = itemView.findViewById(R.id.like_button) val likeButton: MaterialButton = itemView.findViewById(R.id.like_button)
@ -120,29 +120,24 @@ class PostsRecyclerViewAdapter (private val markwon: Markwon, private val callba
return FeedViewHolder(view) return FeedViewHolder(view)
} }
override fun getItemCount(): Int = list.size override fun getItemCount(): Int = feedDto?.data?.size ?: 0
override fun getItemId(position: Int): Long = list[position].post.id
override fun setHasStableIds(hasStableIds: Boolean) { override fun setHasStableIds(hasStableIds: Boolean) {
super.setHasStableIds(true) super.setHasStableIds(true)
} }
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
fun updateList(newList: List<PostDto>?, updateEvent: UpdateEvent? = null) { fun updateList(updatedFeed: FeedDto, updateEvent: UpdateEvent? = null) {
if(updateEvent == null) { if(updateEvent == null) {
if(newList != null) { feedDto = updatedFeed
list = newList
}
notifyDataSetChanged() notifyDataSetChanged()
return return
} }
val postUpdated = list.find { p -> p.post.id == updateEvent.affectedId } ?: return val postUpdated = feedDto?.data?.find { p -> p.post.id == updateEvent.affectedId } ?: return
val position = list.indexOf(postUpdated) val position = feedDto?.data?.indexOf(postUpdated) ?: return
if(newList != null) { feedDto = updatedFeed
list = newList
}
when(updateEvent.updateType) { when(updateEvent.updateType) {
UpdateEvent.UpdateType.POST_LIKED -> { UpdateEvent.UpdateType.POST_LIKED -> {
@ -163,6 +158,6 @@ class PostsRecyclerViewAdapter (private val markwon: Markwon, private val callba
override fun onBindViewHolder(holder: FeedViewHolder, position: Int) {} override fun onBindViewHolder(holder: FeedViewHolder, position: Int) {}
override fun onBindViewHolder(holder: FeedViewHolder, position: Int, payloads: List<Any>) { override fun onBindViewHolder(holder: FeedViewHolder, position: Int, payloads: List<Any>) {
holder.bindView(list[position], payloads) holder.bindView(feedDto?.data?.get(position) ?: return, payloads)
} }
} }

View File

@ -6,11 +6,14 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.isolaatti.posting.comments.data.remote.FeedCommentsDto import com.isolaatti.posting.comments.data.remote.FeedCommentsDto
import com.isolaatti.feed.data.remote.FeedDto import com.isolaatti.posting.posts.data.remote.FeedDto
import com.isolaatti.feed.domain.repository.FeedRepository
import com.isolaatti.posting.likes.data.remote.LikeDto import com.isolaatti.posting.likes.data.remote.LikeDto
import com.isolaatti.posting.likes.domain.repository.LikesRepository import com.isolaatti.posting.likes.domain.repository.LikesRepository
import com.isolaatti.posting.posts.data.repository.PostsRepositoryImpl
import com.isolaatti.posting.posts.domain.PostsRepository
import com.isolaatti.utils.Resource
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -19,11 +22,17 @@ import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class PostsViewModel @Inject constructor(private val feedRepository: FeedRepository, private val likesRepository: LikesRepository) : ViewModel() { class PostsViewModel @Inject constructor(private val postsRepository: PostsRepository, private val likesRepository: LikesRepository) : ViewModel() {
private val _posts: MutableLiveData<FeedDto> = MutableLiveData() private val _posts: MutableLiveData<FeedDto> = MutableLiveData()
val posts: LiveData<FeedDto> get() = _posts val posts: LiveData<FeedDto> get() = _posts
private val _loadingPosts = MutableLiveData(false)
val loadingPosts: LiveData<Boolean> get() = _loadingPosts
private val _errorLoading: MutableLiveData<Resource.Error.ErrorType?> = MutableLiveData()
val errorLoading: LiveData<Resource.Error.ErrorType?> get() = _errorLoading
private val _comments: MutableLiveData<FeedCommentsDto> = MutableLiveData() private val _comments: MutableLiveData<FeedCommentsDto> = MutableLiveData()
val comments: LiveData<FeedCommentsDto> get() = _comments val comments: LiveData<FeedCommentsDto> get() = _comments
@ -34,19 +43,19 @@ class PostsViewModel @Inject constructor(private val feedRepository: FeedReposit
fun getFeed() { fun getFeed() {
viewModelScope.launch { viewModelScope.launch {
feedRepository.getNextPage(getLastId(), 20).onEach {feedDto -> postsRepository.getFeed(getLastId()).onEach {
val temp = _posts.value when(it) {
if(temp != null) { is Resource.Success -> {
feedDto?.data?.let { it -> _loadingPosts.postValue(false)
temp.data.addAll(it) _posts.postValue(posts.value?.concatFeed(it.data) ?: it.data)
} }
temp.let { is Resource.Loading -> {
_posts.postValue(it) _loadingPosts.postValue(true)
}
is Resource.Error -> {
_errorLoading.postValue(it.errorType)
} }
} else {
_posts.postValue(feedDto)
} }
}.flowOn(Dispatchers.IO).launchIn(this) }.flowOn(Dispatchers.IO).launchIn(this)
} }
} }

View File

@ -1,6 +1,5 @@
package com.isolaatti.profile.data.remote package com.isolaatti.profile.data.remote
import com.isolaatti.feed.data.remote.FeedDto
import retrofit2.Call import retrofit2.Call
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Path import retrofit2.http.Path

View File

@ -1,10 +1,19 @@
package com.isolaatti.profile.presentation package com.isolaatti.profile.presentation
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.isolaatti.profile.data.remote.UserProfileDto
import com.isolaatti.profile.domain.ProfileRepository import com.isolaatti.profile.domain.ProfileRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class ProfileViewModel @Inject constructor(private val profileRepository: ProfileRepository) : ViewModel() { class ProfileViewModel @Inject constructor(private val profileRepository: ProfileRepository) : ViewModel() {
private val _profile = MutableLiveData<UserProfileDto>()
val profile: LiveData<UserProfileDto> get() = _profile
fun getProfile(profileId: Int) {
}
} }

View File

@ -2,15 +2,21 @@ package com.isolaatti.profile.ui
import android.os.Bundle import android.os.Bundle
import androidx.activity.addCallback import androidx.activity.addCallback
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.viewpager.widget.PagerAdapter import androidx.viewpager.widget.PagerAdapter
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.adapter.FragmentViewHolder import androidx.viewpager2.adapter.FragmentViewHolder
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import com.isolaatti.R import com.isolaatti.R
import com.isolaatti.databinding.ActivityProfileBinding import com.isolaatti.databinding.ActivityProfileBinding
import com.isolaatti.profile.data.remote.UserProfileDto
import com.isolaatti.profile.presentation.ProfileViewModel
import com.isolaatti.utils.UrlGen
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -38,19 +44,24 @@ class ProfileActivity : AppCompatActivity() {
lateinit var viewBinding: ActivityProfileBinding lateinit var viewBinding: ActivityProfileBinding
private val viewModel: ProfileViewModel by viewModels()
private val profileObserver = Observer<UserProfileDto> { profile ->
Picasso.get()
.load(UrlGen.userProfileImage(profile.id))
.into(viewBinding.profileImageView)
viewBinding.textViewUsername.text = profile.name
viewBinding.textViewDescription.text = profile.descriptionText
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
viewBinding = ActivityProfileBinding.inflate(layoutInflater) viewBinding = ActivityProfileBinding.inflate(layoutInflater)
setContentView(viewBinding.root) setContentView(viewBinding.root)
Picasso.get().load("https://isolaatti.com/api/images/image/63a2a6c5270ecc2be2512799?mode=reduced").into(viewBinding.profileImageView)
viewBinding.textViewUsername.text = "Erik Everardo"
viewBinding.textViewDescription.text = "Hola"
viewBinding.profileViewPager2.adapter = ViewPagerAdapter(this) viewBinding.profileViewPager2.adapter = ViewPagerAdapter(this)
viewBinding.topAppBar.setNavigationOnClickListener { viewBinding.topAppBar.setNavigationOnClickListener {
onBackPressed() finish()
} }
TabLayoutMediator(viewBinding.profileTabLayout, viewBinding.profileViewPager2) {tab, position -> TabLayoutMediator(viewBinding.profileTabLayout, viewBinding.profileViewPager2) {tab, position ->
@ -66,5 +77,11 @@ class ProfileActivity : AppCompatActivity() {
} }
} }
}.attach() }.attach()
viewModel.profile.observe(this, profileObserver)
}
companion object {
const val EXTRA_USER_ID = "user_id"
} }
} }

View File

@ -1,9 +1,11 @@
package com.isolaatti.utils package com.isolaatti.utils
class Resource<T> { abstract class Resource<T> {
inner class Success(data: T) class Success<T>(val data: T?): Resource<T>()
class Loading<T>: Resource<T>()
inner class Loading() class Error<T>(val errorType: ErrorType? = null): Resource<T>() {
enum class ErrorType {
inner class Error NetworkError, AuthError, NotFoundError, ServerError, OtherError
}
}
} }

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
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,15c1.66,0 2.99,-1.34 2.99,-3L15,6c0,-1.66 -1.34,-3 -3,-3S9,4.34 9,6v6c0,1.66 1.34,3 3,3zM17.3,12c0,3 -2.54,5.1 -5.3,5.1S6.7,15 6.7,12L5,12c0,3.42 2.72,6.23 6,6.72L11,22h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
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,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11L5,11c0,3.41 2.72,6.23 6,6.72L11,21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector android:height="24dp" android:tint="#000000"
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="M17,19.22H5V7h7V5H5C3.9,5 3,5.9 3,7v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-7h-2V19.22z"/>
<path android:fillColor="@android:color/white" android:pathData="M19,2h-2v3h-3c0.01,0.01 0,2 0,2h3v2.99c0.01,0.01 2,0 2,0V7h3V5h-3V2z"/>
<path android:fillColor="@android:color/white" android:pathData="M7,9h8v2h-8z"/>
<path android:fillColor="@android:color/white" android:pathData="M7,12l0,2l8,0l0,-2l-3,0z"/>
<path android:fillColor="@android:color/white" android:pathData="M7,15h8v2h-8z"/>
</vector>

View File

@ -17,7 +17,7 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/home_drawer" app:layout_constraintStart_toEndOf="@+id/home_drawer"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:context=".feed.ui.FeedFragment"> tools:context=".home.FeedFragment">
@ -48,6 +48,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
app:title="@string/app_name" app:title="@string/app_name"
app:titleCentered="true" app:titleCentered="true"
app:menu="@menu/new_menu"
tools:layout_conversion_absoluteHeight="64dp" tools:layout_conversion_absoluteHeight="64dp"
tools:layout_conversion_absoluteWidth="531dp" tools:layout_conversion_absoluteWidth="531dp"
tools:layout_editor_absoluteX="360dp" tools:layout_editor_absoluteX="360dp"

View File

@ -1,92 +1,105 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<ScrollView <FrameLayout android:layout_width="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_height="match_parent"
android:layout_width="match_parent" xmlns:android="http://schemas.android.com/apk/res/android">
android:layout_height="match_parent">
<LinearLayout <ProgressBar
xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/progressBar"
android:orientation="vertical" style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:translationZ="10dp"
android:visibility="gone" />
<ScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="match_parent">
<LinearLayout
<TextView xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:textAlignment="center"
android:textAppearance="?attr/textAppearanceHeadlineLarge"
android:layout_margin="24dp"
android:text="@string/app_name"/>
<TextView <TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceHeadlineSmall"
android:layout_margin="24dp"
android:text="@string/sign_in"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textFieldEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="8dp"
android:hint="@string/email">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:inputType="textEmailAddress" android:textAlignment="center"
/> android:textAppearance="?attr/textAppearanceHeadlineLarge"
android:layout_margin="24dp"
android:text="@string/app_name"/>
</com.google.android.material.textfield.TextInputLayout> <TextView
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textFieldPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="8dp"
app:endIconMode="password_toggle"
android:hint="@string/password">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:inputType="textPassword" android:textAppearance="?attr/textAppearanceHeadlineSmall"
/> android:layout_margin="24dp"
android:text="@string/sign_in"/>
</com.google.android.material.textfield.TextInputLayout> <com.google.android.material.textfield.TextInputLayout
android:id="@+id/textFieldEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="8dp"
android:hint="@string/email">
<Button <com.google.android.material.textfield.TextInputEditText
android:id="@+id/sign_in_btn" android:layout_width="match_parent"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:inputType="textEmailAddress"
android:text="@string/sign_in" />
android:layout_margin="8dp"/>
<Button </com.google.android.material.textfield.TextInputLayout>
android:id="@+id/forgot_password_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_gravity="center_horizontal"
android:text="@string/forgot_password"
style="@style/Widget.Material3.Button.TextButton"/>
<TextView <com.google.android.material.textfield.TextInputLayout
android:layout_width="wrap_content" android:id="@+id/textFieldPassword"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:text="@string/or_you_can_sign_up" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:layout_marginHorizontal="16dp"
android:layout_marginTop="24dp"/> android:layout_marginVertical="8dp"
app:endIconMode="password_toggle"
android:hint="@string/password">
<Button <com.google.android.material.textfield.TextInputEditText
android:id="@+id/sign_up_btn" android:layout_width="match_parent"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:inputType="textPassword"
android:layout_gravity="center_horizontal" />
android:layout_marginTop="24dp"
android:text="@string/sign_up"/>
</LinearLayout> </com.google.android.material.textfield.TextInputLayout>
</ScrollView>
<Button
android:id="@+id/sign_in_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/sign_in"
android:layout_margin="8dp"/>
<Button
android:id="@+id/forgot_password_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_gravity="center_horizontal"
android:text="@string/forgot_password"
style="@style/Widget.Material3.Button.TextButton"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/or_you_can_sign_up"
android:layout_gravity="center_horizontal"
android:layout_marginTop="24dp"/>
<Button
android:id="@+id/sign_up_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="24dp"
android:text="@string/sign_up"/>
</LinearLayout>
</ScrollView>
</FrameLayout>

View File

@ -6,78 +6,126 @@
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/topAppBar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/top_app_bar"
style="@style/Theme.Isolaatti"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/baseline_arrow_back_24"
app:title="Profile"
app:titleCentered="true" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="match_parent"> android:layout_height="0dp"
<LinearLayout app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/topAppBar_layout">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent">
android:orientation="vertical">
<LinearLayout <com.google.android.material.imageview.ShapeableImageView
android:id="@+id/profile_image_view"
android:layout_width="120dp"
android:layout_height="120dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"
tools:srcCompat="@tools:sample/avatars" />
<TextView
android:id="@+id/text_view_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_weight="1"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/profile_image_view"
tools:text="Erik Cavazos" />
<TextView
android:id="@+id/text_view_description"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center_horizontal" android:layout_marginHorizontal="20dp"
android:orientation="vertical"> android:maxLines="4"
<com.google.android.material.appbar.AppBarLayout android:textAlignment="center"
android:id="@+id/topAppBar_layout" app:layout_constraintTop_toBottomOf="@id/text_view_username"
android:layout_width="match_parent" app:layout_constraintStart_toStartOf="parent"
android:layout_height="wrap_content"> app:layout_constraintEnd_toEndOf="parent"
tools:text="Hi there, I am software developer!" />
<com.google.android.material.appbar.MaterialToolbar <TextView
android:id="@+id/top_app_bar" android:id="@+id/text_view_following_state"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="wrap_content"
app:titleCentered="true" android:layout_marginTop="20dp"
style="@style/Theme.Isolaatti" android:textAlignment="center"
app:navigationIcon="@drawable/baseline_arrow_back_24" android:textStyle="bold"
app:title="Profile"/> app:layout_constraintTop_toBottomOf="@id/text_view_description"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:text="Following this profile" />
<com.google.android.material.button.MaterialButton
style="?attr/materialIconButtonFilledTonalStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toStartOf="@id/profile_options_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_view_following_state" />
</com.google.android.material.appbar.AppBarLayout> <com.google.android.material.button.MaterialButton
android:id="@+id/profile_options_button"
style="?attr/materialIconButtonFilledTonalStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
app:icon="@drawable/baseline_more_horiz_24"
<com.google.android.material.imageview.ShapeableImageView app:layout_constraintEnd_toEndOf="parent"
android:id="@+id/profile_image_view" app:layout_constraintTop_toBottomOf="@id/text_view_following_state"/>
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_weight="1"
android:layout_marginTop="16dp"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"
tools:srcCompat="@tools:sample/avatars" />
<TextView
android:id="@+id/text_view_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="20sp"
android:layout_marginTop="20dp"
tools:text="Erik Cavazos" />
<TextView
android:id="@+id/text_view_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:textAlignment="center"
tools:text="Hi there, I am software developer!"/>
</LinearLayout>
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
android:id="@+id/profile_tab_layout" android:id="@+id/profile_tab_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:tabContentStart="56dp"
app:tabMode="fixed" app:tabMode="fixed"
app:tabContentStart="56dp"/> app:layout_constraintTop_toBottomOf="@id/profile_options_button"/>
<androidx.viewpager2.widget.ViewPager2 <androidx.viewpager2.widget.ViewPager2
android:id="@+id/profile_view_pager2" android:id="@+id/profile_view_pager2"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" /> android:layout_height="wrap_content"
</LinearLayout> app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/profile_tab_layout"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>

View File

@ -0,0 +1,35 @@
<?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"
xmlns:app="http://schemas.android.com/apk/res-auto">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/topAppBar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/topAppBar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:theme="@style/Theme.Isolaatti"
app:navigationIcon="@drawable/baseline_close_24"
app:navigationIconTint="@color/on_surface"
app:title="@string/audio_recorder"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:name="androidx.navigation.fragment.NavHostFragment"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/topAppBar_layout" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

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

View File

@ -58,7 +58,7 @@
app:boxCornerRadiusTopEnd="20dp" app:boxCornerRadiusTopEnd="20dp"
app:boxCornerRadiusTopStart="20dp" app:boxCornerRadiusTopStart="20dp"
app:startIconDrawable="@drawable/baseline_open_in_full_24" app:startIconDrawable="@drawable/baseline_keyboard_voice_24"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/submitCommentButton" app:layout_constraintEnd_toStartOf="@+id/submitCommentButton"

View File

@ -13,7 +13,7 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".feed.ui.FeedFragment"> tools:context=".home.FeedFragment">
<FrameLayout <FrameLayout
@ -29,7 +29,8 @@
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/topAppBar_layout" android:id="@+id/topAppBar_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/topAppBar" android:id="@+id/topAppBar"
@ -38,26 +39,23 @@
android:theme="@style/Theme.Isolaatti" android:theme="@style/Theme.Isolaatti"
app:navigationIcon="@drawable/baseline_menu_24" app:navigationIcon="@drawable/baseline_menu_24"
app:navigationIconTint="@color/on_surface" app:navigationIconTint="@color/on_surface"
app:menu="@menu/new_menu"
app:title="@string/app_name" app:title="@string/app_name"
app:titleCentered="true" /> app:titleCentered="true" />
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/feed_recycler_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" >
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/feed_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</LinearLayout> </LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/floating_action_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:contentDescription=""
app:srcCompat="@drawable/baseline_add_24" />
</FrameLayout> </FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,17 +1,22 @@
<?xml version="1.0" encoding="utf-8"?> <?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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="wrap_content">
<TextView <ScrollView
android:id="@+id/textView3" android:layout_width="match_parent"
android:layout_width="wrap_content" android:layout_height="match_parent">
android:layout_height="wrap_content"
android:text="@string/images" <TextView
app:layout_constraintBottom_toBottomOf="parent" android:id="@+id/textView3"
app:layout_constraintEnd_toEndOf="parent" android:layout_width="wrap_content"
app:layout_constraintStart_toStartOf="parent" android:layout_height="2000dp"
app:layout_constraintTop_toTopOf="parent" /> android:text="@string/images"
</androidx.constraintlayout.widget.ConstraintLayout> app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</ScrollView>
</FrameLayout>

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- NestedScrollingChild goes here (NestedScrollView, RecyclerView, etc.). -->
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.search.SearchBar$ScrollingViewBehavior">
<!-- Screen content goes here. -->
</androidx.core.widget.NestedScrollView>
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.search.SearchBar
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/searchbar_hint" />
</com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.search.SearchView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="@string/searchbar_hint"
app:layout_anchor="@id/search_bar">
<!-- Search suggestions/results go here (ScrollView, RecyclerView, etc.). -->
</com.google.android.material.search.SearchView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -3,7 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"> xmlns:android="http://schemas.android.com/apk/res/android">
<item <item
android:icon="@drawable/baseline_notifications_24" android:icon="@drawable/baseline_add_24"
android:title="Notifications" app:showAsAction="always"
app:showAsAction="always" /> android:title="@string/new_post" />
</menu> </menu>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/menu_item_new_discussion"
android:title="@string/discussion"
android:icon="@drawable/baseline_post_add_24"
app:showAsAction="ifRoom"/>
<item android:id="@+id/menu_item_new_audio"
android:title="@string/add_audio"
android:icon="@drawable/baseline_mic_24"
app:showAsAction="ifRoom"/>
</menu>

View File

@ -0,0 +1,14 @@
<?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"
android:id="@+id/audio_recorder_navigation"
app:startDestination="@id/audioRecorderMainFragment">
<fragment
android:id="@+id/audioRecorderMainFragment"
android:name="com.isolaatti.audio.recorder.ui.AudioRecorderMainFragment"
android:label="AudioRecorderMainFragment" />
<fragment
android:id="@+id/audioDraftsFragment"
android:name="com.isolaatti.audio.recorder.ui.AudioDraftsFragment"
android:label="AudioDraftsFragment" />
</navigation>

View File

@ -7,12 +7,28 @@
<fragment <fragment
android:id="@+id/feedFragment" android:id="@+id/feedFragment"
android:name="com.isolaatti.feed.ui.FeedFragment" android:name="com.isolaatti.home.FeedFragment"
android:label="fragment_feed" android:label="fragment_feed"
tools:layout="@layout/fragment_feed" /> tools:layout="@layout/fragment_feed" >
<action
android:id="@+id/action_feedFragment_to_profileActivity"
app:destination="@id/profileActivity" />
</fragment>
<fragment <fragment
android:id="@+id/notificationsFragment" android:id="@+id/notificationsFragment"
android:name="com.isolaatti.home.notifications.ui.NotificationsFragment" android:name="com.isolaatti.home.notifications.ui.NotificationsFragment"
android:label="fragment_notifications" android:label="fragment_notifications"
tools:layout="@layout/fragment_notifications" /> tools:layout="@layout/fragment_notifications" >
<action
android:id="@+id/action_notificationsFragment_to_profileActivity"
app:destination="@id/profileActivity" />
</fragment>
<activity
android:id="@+id/profileActivity"
android:name="com.isolaatti.profile.ui.ProfileActivity"
android:label="ProfileActivity" />
<fragment
android:id="@+id/searchFragment"
android:name="com.isolaatti.home.search.presentation.SearchFragment"
android:label="@string/search" />
</navigation> </navigation>

View File

@ -18,9 +18,27 @@
<string name="report">Report</string> <string name="report">Report</string>
<string name="close">Close</string> <string name="close">Close</string>
<!-- Common -->
<string name="accept">Accept</string>
<string name="no">No</string>
<!-- error messages-->
<string name="need_reauth_message">Your session is not valid, please enter your credentials.</string>
<string name="network_error">Network error occurred, it maybe is on our side. Please try again later</string>
<string name="server_error">An error on our side has occurred.</string>
<!--Post options --> <!--Post options -->
<string name="post_options_title">Discussion options</string> <string name="post_options_title">Discussion options</string>
<string name="home">Home</string> <string name="home">Home</string>
<string name="notifications">Notifications</string> <string name="notifications">Notifications</string>
<string name="search">Search</string> <string name="search">Search</string>
<!-- Audio recorder -->
<string name="audio_recorder">Record audio</string>
<string name="account_not_found">Could not find any account that matches %s</string>
<string name="dismiss">Dismiss</string>
<string name="wrong_password">Could not sign in, password does not match</string>
<string name="new_post">New</string>
<string name="discussion">Discussion</string>
<string name="add_audio">New audio</string>
</resources> </resources>

View File

@ -1,7 +1,13 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
dependencies {
classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1"
}
}
plugins { plugins {
id 'com.android.application' version '8.0.2' apply false id 'com.android.application' version '8.0.2' apply false
id 'com.android.library' version '8.0.2' apply false id 'com.android.library' version '8.0.2' apply false
id 'org.jetbrains.kotlin.android' version '1.8.0' apply false id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
id 'com.google.dagger.hilt.android' version '2.44' apply false id 'com.google.dagger.hilt.android' version '2.44' apply false
} }