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>
<option name="values">
<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>
</option>
</PersistentState>
@ -28,8 +28,8 @@
</option>
<option name="values">
<map>
<entry key="outputName" value="baseline_search_24" />
<entry key="sourceFile" value="C:\Users\erike\Downloads\comments-solid.svg" />
<entry key="outputName" value="baseline_mic_24" />
<entry key="sourceFile" value="$USER_HOME$/C:\Users\erike\Downloads\comments-solid.svg" />
</map>
</option>
</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 'com.google.dagger.hilt.android'
id 'org.jetbrains.kotlin.plugin.serialization' version '1.8.0'
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
}
android {
@ -89,6 +90,19 @@ dependencies {
implementation "io.noties.markwon:editor:$markwon_version"
implementation "io.noties.markwon:image-picasso:$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 {

View File

@ -11,11 +11,12 @@
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.Isolaatti"
android:usesCleartextTraffic="true"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.Isolaatti.Splash" android:noHistory="true">
android:theme="@style/Theme.Isolaatti.Splash">
<intent-filter>
<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
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import com.isolaatti.auth.data.AuthRepositoryImpl
import com.isolaatti.home.HomeActivity
@ -15,6 +17,14 @@ class MainActivity : ComponentActivity() {
@Inject
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?) {
var isLoading = true
val splashScreen = installSplashScreen()
@ -25,11 +35,12 @@ class MainActivity : ComponentActivity() {
val currentToken = authRepository.getCurrentToken()
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 {
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
}

View File

@ -1,6 +1,8 @@
package com.isolaatti
import android.app.Activity
import android.app.Application
import android.os.Bundle
import com.isolaatti.auth.data.AuthRepositoryImpl
import com.isolaatti.auth.data.local.TokenStorage
import com.isolaatti.auth.data.remote.AuthApi
@ -13,7 +15,15 @@ import javax.inject.Singleton
@HiltAndroidApp
class MyApplication : Application() {
private val activityLifecycleCallbacks = ActivityLifecycleCallbacks()
override fun 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
import android.util.Log
import com.isolaatti.auth.data.remote.AuthTokenDto
import com.isolaatti.auth.data.local.TokenStorage
import com.isolaatti.auth.data.remote.AuthApi
import com.isolaatti.auth.data.remote.Credential
import com.isolaatti.auth.domain.AuthRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import retrofit2.await
import retrofit2.awaitResponse
import java.io.IOException
import javax.inject.Inject
@ -20,22 +18,41 @@ class AuthRepositoryImpl @Inject constructor(
private val tokenStorage: TokenStorage,
private val authApi: AuthApi
) : AuthRepository {
override fun authWithEmailAndPassword(email: String, password: String): Flow<Boolean> = flow {
val res = authApi.signInWithEmailAndPassword(Credential(email, password)).awaitResponse()
val authDto = res.body()
override fun authWithEmailAndPassword(
email: String,
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) {
tokenStorage.storeToken(authDto)
emit(true)
} else {
emit(false)
when(res.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 logout(): Flow<Boolean> = flow {
tokenStorage.removeToken()
authApi.signOut()
try {
authApi.signOut().awaitResponse()
} catch(_: Exception) { }
emit(true)
}

View File

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

View File

@ -1,6 +1,6 @@
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 {
return token
}

View File

@ -1,10 +1,11 @@
package com.isolaatti.auth.domain
import com.isolaatti.auth.data.remote.AuthTokenDto
import com.isolaatti.utils.Resource
import kotlinx.coroutines.flow.Flow
interface AuthRepository {
fun authWithEmailAndPassword(email: String, password: String): Flow<Boolean>
fun authWithEmailAndPassword(email: String, password: String): Flow<Resource<Boolean>>
fun logout(): Flow<Boolean>
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
import com.isolaatti.BuildConfig
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
@ -14,7 +15,7 @@ class RetrofitClient @Inject constructor(private val authenticationInterceptor:
val excludedUrlsFromAuthentication = listOf(
"/api/LogIn"
)
const val BASE_URL = "https://isolaatti.com/api/"
const val BASE_URL = "${BuildConfig.backend}/api/"
}
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.os.Bundle
@ -10,6 +10,7 @@ import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.isolaatti.R
import com.isolaatti.common.ErrorMessageViewModel
import com.isolaatti.databinding.FragmentFeedBinding
import com.isolaatti.posting.posts.presentation.PostsViewModel
import com.isolaatti.posting.comments.presentation.BottomSheetPostComments
@ -34,6 +35,7 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
}
private val viewModel: PostsViewModel by activityViewModels()
private val errorViewModel: ErrorMessageViewModel by activityViewModels()
private lateinit var viewBinding: FragmentFeedBinding
private lateinit var adapter: PostsRecyclerViewAdapter
@ -67,6 +69,7 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
}
}
val markwon = Markwon.builder(requireContext())
.usePlugin(object: AbstractMarkwonPlugin() {
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
@ -78,20 +81,32 @@ class FeedFragment : Fragment(), OnUserInteractedWithPostCallback {
.usePlugin(PicassoImagesPluginDef.picassoImagePlugin)
.usePlugin(LinkifyPlugin.create())
.build()
adapter = PostsRecyclerViewAdapter(markwon, this, listOf())
adapter = PostsRecyclerViewAdapter(markwon, this, null)
viewBinding.feedRecyclerView.adapter = adapter
viewBinding.feedRecyclerView.layoutManager = LinearLayoutManager(requireContext())
viewModel.posts.observe(viewLifecycleOwner){
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) {
adapter.updateList(viewModel.posts.value?.data, PostsRecyclerViewAdapter.UpdateEvent(
viewModel.posts.value?.let { feed ->
adapter.updateList(
feed, PostsRecyclerViewAdapter.UpdateEvent(
PostsRecyclerViewAdapter.UpdateEvent.UpdateType.POST_LIKED, it.postId))
}
}
}
fun onNewMenuItemClicked(v: View) {
}
override fun onLiked(postId: Long) = viewModel.likePost(postId)

View File

@ -4,17 +4,24 @@ import android.os.Bundle
import android.view.Menu
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.setupWithNavController
import com.isolaatti.R
import com.isolaatti.common.ErrorMessageViewModel
import com.isolaatti.common.IsolaattiBaseActivity
import com.isolaatti.databinding.ActivityHomeBinding
import com.isolaatti.posting.posts.presentation.PostsViewModel
import com.isolaatti.utils.Resource
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class HomeActivity : AppCompatActivity() {
lateinit var viewBinding: ActivityHomeBinding
val postsViewModel: PostsViewModel by viewModels()
class HomeActivity : IsolaattiBaseActivity() {
private lateinit var viewBinding: ActivityHomeBinding
private val postsViewModel: PostsViewModel by viewModels()
override fun onRetry() {
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -33,4 +40,8 @@ class HomeActivity : AppCompatActivity() {
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
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
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.widget.doOnTextChanged
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.home.HomeActivity
import com.isolaatti.utils.Resource
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class LogInActivity: AppCompatActivity() {
lateinit var viewBinding: ActivityLoginBinding
val viewModel: LogInViewModel by viewModels()
private val viewModel: LogInViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -23,11 +34,33 @@ class LogInActivity: AppCompatActivity() {
setContentView(viewBinding.root)
viewModel.signInSuccess.observe(this) {success ->
if(success)
startActivity(Intent(this, HomeActivity::class.java))
else
Toast.makeText(this,"Could not sign in, your credential is not correct...", Toast.LENGTH_SHORT).show()
// Show login error message.
if(success) {
setResult(Activity.RESULT_OK)
finish()
}
}
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 ->
@ -50,5 +83,42 @@ class LogInActivity: AppCompatActivity() {
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.viewModelScope
import com.isolaatti.auth.domain.AuthRepository
import com.isolaatti.utils.Resource
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
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 javax.inject.Inject
@HiltViewModel
class LogInViewModel @Inject constructor(private val authRepository: AuthRepository): ViewModel() {
val signInSuccess: MutableLiveData<Boolean> = MutableLiveData()
val signInError: MutableLiveData<Resource.Error.ErrorType?> = MutableLiveData()
val signInLoading: MutableLiveData<Boolean> = MutableLiveData()
val formIsValid: MutableLiveData<Boolean> = MutableLiveData(false)
val emailUserInputIsValid: 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) {
signInError.postValue(null)
viewModelScope.launch {
authRepository.authWithEmailAndPassword(email, password).collect {
authRepository.authWithEmailAndPassword(email, password).onEach {
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
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.domain.PostsRepository
import dagger.Module
@ -13,12 +13,12 @@ import dagger.hilt.components.SingletonComponent
@InstallIn(SingletonComponent::class)
class Module {
@Provides
fun providePostsApi(retrofitClient: RetrofitClient): PostsApi {
return retrofitClient.client.create(PostsApi::class.java)
fun providePostsApi(retrofitClient: RetrofitClient): FeedsApi {
return retrofitClient.client.create(FeedsApi::class.java)
}
@Provides
fun providePostsRepository(postsApi: PostsApi): PostsRepository {
return PostsRepositoryImpl(postsApi)
fun providePostsRepository(feedsApi: FeedsApi): PostsRepository {
return PostsRepositoryImpl(feedsApi)
}
}

View File

@ -94,7 +94,7 @@ class BottomSheetPostComments() : BottomSheetDialogFragment(), OnUserInteractedC
val textField = viewBinding.newCommentTextField
textField.setStartIconOnClickListener {
AlertDialog.Builder(requireContext()).setView(R.layout.write_comment_multiline_dialog).show()
}
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
import com.isolaatti.feed.data.remote.FeedDto
import com.isolaatti.feed.data.remote.PostDto
import com.isolaatti.profile.data.remote.ProfileListItemDto
import retrofit2.Call
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
interface PostsApi {
interface FeedsApi {
@GET("Fetch/PostsOfUser/{userId}")
fun postsOfUser(@Path("userId") userId: Int,
@Query("length") length: Int,
@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}")
fun getPost(@Path("postId") postId: Long): Call<PostDto>
fun getPost(@Path("postId") postId: Long): Call<FeedDto>
@GET("Fetch/Post/{postId}/LikedBy")
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
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.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 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
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 {
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 com.google.android.material.button.MaterialButton
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.utils.UrlGen.userProfileImage
import com.squareup.picasso.Picasso
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) {
fun bindView(postDto: PostDto, payloads: List<Any>) {
fun bindView(postDto: FeedDto.PostDto, payloads: List<Any>) {
Log.d("payloads", payloads.count().toString())
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)
}
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) {
super.setHasStableIds(true)
}
@SuppressLint("NotifyDataSetChanged")
fun updateList(newList: List<PostDto>?, updateEvent: UpdateEvent? = null) {
fun updateList(updatedFeed: FeedDto, updateEvent: UpdateEvent? = null) {
if(updateEvent == null) {
if(newList != null) {
list = newList
}
feedDto = updatedFeed
notifyDataSetChanged()
return
}
val postUpdated = list.find { p -> p.post.id == updateEvent.affectedId } ?: return
val position = list.indexOf(postUpdated)
val postUpdated = feedDto?.data?.find { p -> p.post.id == updateEvent.affectedId } ?: return
val position = feedDto?.data?.indexOf(postUpdated) ?: return
if(newList != null) {
list = newList
}
feedDto = updatedFeed
when(updateEvent.updateType) {
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, 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.viewModelScope
import com.isolaatti.posting.comments.data.remote.FeedCommentsDto
import com.isolaatti.feed.data.remote.FeedDto
import com.isolaatti.feed.domain.repository.FeedRepository
import com.isolaatti.posting.posts.data.remote.FeedDto
import com.isolaatti.posting.likes.data.remote.LikeDto
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
@ -19,11 +22,17 @@ import kotlinx.coroutines.launch
import javax.inject.Inject
@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()
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()
val comments: LiveData<FeedCommentsDto> get() = _comments
@ -34,19 +43,19 @@ class PostsViewModel @Inject constructor(private val feedRepository: FeedReposit
fun getFeed() {
viewModelScope.launch {
feedRepository.getNextPage(getLastId(), 20).onEach {feedDto ->
val temp = _posts.value
if(temp != null) {
feedDto?.data?.let { it ->
temp.data.addAll(it)
postsRepository.getFeed(getLastId()).onEach {
when(it) {
is Resource.Success -> {
_loadingPosts.postValue(false)
_posts.postValue(posts.value?.concatFeed(it.data) ?: it.data)
}
temp.let {
_posts.postValue(it)
is Resource.Loading -> {
_loadingPosts.postValue(true)
}
is Resource.Error -> {
_errorLoading.postValue(it.errorType)
}
} else {
_posts.postValue(feedDto)
}
}.flowOn(Dispatchers.IO).launchIn(this)
}
}

View File

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

View File

@ -1,10 +1,19 @@
package com.isolaatti.profile.presentation
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.isolaatti.profile.data.remote.UserProfileDto
import com.isolaatti.profile.domain.ProfileRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
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 androidx.activity.addCallback
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.viewpager.widget.PagerAdapter
import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.adapter.FragmentViewHolder
import com.google.android.material.tabs.TabLayoutMediator
import com.isolaatti.R
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 dagger.hilt.android.AndroidEntryPoint
@ -38,19 +44,24 @@ class ProfileActivity : AppCompatActivity() {
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?) {
super.onCreate(savedInstanceState)
viewBinding = ActivityProfileBinding.inflate(layoutInflater)
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.topAppBar.setNavigationOnClickListener {
onBackPressed()
finish()
}
TabLayoutMediator(viewBinding.profileTabLayout, viewBinding.profileViewPager2) {tab, position ->
@ -66,5 +77,11 @@ class ProfileActivity : AppCompatActivity() {
}
}
}.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
class Resource<T> {
inner class Success(data: T)
inner class Loading()
inner class Error
abstract class Resource<T> {
class Success<T>(val data: T?): Resource<T>()
class Loading<T>: Resource<T>()
class Error<T>(val errorType: ErrorType? = null): Resource<T>() {
enum class ErrorType {
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_constraintStart_toEndOf="@+id/home_drawer"
app:layout_constraintTop_toTopOf="parent"
tools:context=".feed.ui.FeedFragment">
tools:context=".home.FeedFragment">
@ -48,6 +48,7 @@
android:layout_height="match_parent"
app:title="@string/app_name"
app:titleCentered="true"
app:menu="@menu/new_menu"
tools:layout_conversion_absoluteHeight="64dp"
tools:layout_conversion_absoluteWidth="531dp"
tools:layout_editor_absoluteX="360dp"

View File

@ -1,6 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android">
<ProgressBar
android:id="@+id/progressBar"
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
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
@ -90,3 +102,4 @@
</LinearLayout>
</ScrollView>
</FrameLayout>

View File

@ -6,47 +6,49 @@
android:layout_height="match_parent">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
<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"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical">
<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/top_app_bar"
style="@style/Theme.Isolaatti"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:titleCentered="true"
style="@style/Theme.Isolaatti"
app:navigationIcon="@drawable/baseline_arrow_back_24"
app:title="Profile"/>
app:title="Profile"
app:titleCentered="true" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/topAppBar_layout">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/profile_image_view"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_weight="1"
android:layout_marginTop="16dp"
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" />
@ -54,30 +56,76 @@
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"
android:layout_marginTop="20dp"
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_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:maxLines="4"
android:textAlignment="center"
app:layout_constraintTop_toBottomOf="@id/text_view_username"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:text="Hi there, I am software developer!" />
</LinearLayout>
<TextView
android:id="@+id/text_view_following_state"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:textAlignment="center"
android:textStyle="bold"
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.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"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_view_following_state"/>
<com.google.android.material.tabs.TabLayout
android:id="@+id/profile_tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tabContentStart="56dp"
app:tabMode="fixed"
app:tabContentStart="56dp"/>
app:layout_constraintTop_toBottomOf="@id/profile_options_button"/>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/profile_view_pager2"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/profile_tab_layout"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</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:boxCornerRadiusTopStart="20dp"
app:startIconDrawable="@drawable/baseline_open_in_full_24"
app:startIconDrawable="@drawable/baseline_keyboard_voice_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/submitCommentButton"

View File

@ -13,7 +13,7 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".feed.ui.FeedFragment">
tools:context=".home.FeedFragment">
<FrameLayout
@ -29,7 +29,8 @@
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/topAppBar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/topAppBar"
@ -38,26 +39,23 @@
android:theme="@style/Theme.Isolaatti"
app:navigationIcon="@drawable/baseline_menu_24"
app:navigationIconTint="@color/on_surface"
app:menu="@menu/new_menu"
app:title="@string/app_name"
app:titleCentered="true" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:layout_width="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>
<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>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,17 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_height="2000dp"
android:text="@string/images"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</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">
<item
android:icon="@drawable/baseline_notifications_24"
android:title="Notifications"
app:showAsAction="always" />
android:icon="@drawable/baseline_add_24"
app:showAsAction="always"
android:title="@string/new_post" />
</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
android:id="@+id/feedFragment"
android:name="com.isolaatti.feed.ui.FeedFragment"
android:name="com.isolaatti.home.FeedFragment"
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
android:id="@+id/notificationsFragment"
android:name="com.isolaatti.home.notifications.ui.NotificationsFragment"
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>

View File

@ -18,9 +18,27 @@
<string name="report">Report</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 -->
<string name="post_options_title">Discussion options</string>
<string name="home">Home</string>
<string name="notifications">Notifications</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>

View File

@ -1,7 +1,13 @@
// 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 {
id 'com.android.application' 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 'com.google.dagger.hilt.android' version '2.44' apply false
}