Skip to content

Release 1.64 #1103

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 0 additions & 25 deletions androidHyperskillApp/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.DETECT_SCREEN_CAPTURE" />

<uses-permission
android:name="android.permission.WAKE_LOCK"
android:maxSdkVersion="26"/>

<application
android:label="@string/android_app_name"
android:name=".HyperskillApp"
Expand All @@ -35,24 +28,6 @@
</intent-filter>
</activity>

<receiver android:name=".notification.local.receiver.AlarmReceiver" />

<service
android:name=".notification.local.service.RescheduleLocalNotificationsService"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" />

<receiver
android:name=".notification.local.receiver.RescheduleNotificationsReceiver"
android:enabled="true"
android:exported="true"
android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>

<service
android:name=".notification.remote.service.HyperskillFcmService"
android:exported="false">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package org.hyperskill.app.android.comments.fragment

import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.Window
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.view.WindowCompat
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.ViewModelProvider
import org.hyperskill.app.android.HyperskillApp
import org.hyperskill.app.android.R
import org.hyperskill.app.android.comments.ui.Comments
import org.hyperskill.app.android.core.extensions.argument
import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme
import org.hyperskill.app.comments.presentation.CommentsViewModel
import org.hyperskill.app.comments.screen.domain.model.CommentsScreenFeatureParams
import org.hyperskill.app.comments.screen.presentation.CommentsScreenFeature
import org.hyperskill.app.core.view.handleActions

class CommentsDialogFragment : DialogFragment() {
companion object {
const val TAG = "CommentsDialogFragment"

fun newInstance(params: CommentsScreenFeatureParams): CommentsDialogFragment =
CommentsDialogFragment().apply {
this.params = params
}
}

private var params: CommentsScreenFeatureParams by argument(CommentsScreenFeatureParams.serializer())

private var viewModelFactory: ViewModelProvider.Factory? = null
private val commentsViewModel: CommentsViewModel by viewModels { requireNotNull(viewModelFactory) }

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, R.style.ThemeOverlay_AppTheme_Dialog_Fullscreen)
injectComponent()
commentsViewModel.handleActions(this, ::handleActions)
}

private fun injectComponent() {
val component = HyperskillApp.graph().buildPlatformCommentsScreenComponent(params)
viewModelFactory = component.reduxViewModelFactory
}

override fun onStart() {
super.onStart()
dialog
?.window
?.let { window ->
window.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
WindowCompat.setDecorFitsSystemWindows(window, false)
window.setWindowAnimations(R.style.ThemeOverlay_AppTheme_Dialog_Fullscreen)
}
}

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog =
super.onCreateDialog(savedInstanceState).apply {
setCanceledOnTouchOutside(false)
requestWindowFeature(Window.FEATURE_NO_TITLE)
setOnShowListener {
commentsViewModel.onNewMessage(CommentsScreenFeature.Message.ViewedEventMessage)
}
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View =
ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnLifecycleDestroyed(viewLifecycleOwner))
setContent {
HyperskillTheme {
Comments(
viewModel = commentsViewModel,
onCloseClick = ::onCloseClick
)
}
}
}

private fun onCloseClick() {
dismiss()
}

@Suppress("UnusedParameter")
private fun handleActions(action: CommentsScreenFeature.Action.ViewAction) {
// no op
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package org.hyperskill.app.android.comments.ui

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.hyperskill.app.R
import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme
import org.hyperskill.app.comments.screen.view.model.CommentsScreenViewState
import org.hyperskill.app.reactions.domain.model.ReactionType

@Composable
fun Comment(
comment: CommentsScreenViewState.CommentItem,
isShowRepliesBtnVisible: Boolean,
onShowRepliesClick: (Long) -> Unit,
onReactionClick: (Long, ReactionType) -> Unit,
modifier: Modifier = Modifier
) {
val currentOnReactionClick by rememberUpdatedState { reactionType: ReactionType ->
onReactionClick(comment.id, reactionType)
}
Column(
modifier = modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(CommentDefaults.CommentContentVerticalPadding)
) {
CommentHeader(
authorAvatar = comment.authorAvatar,
authorFullName = comment.authorFullName,
formattedTime = comment.formattedTime
)
SelectionContainer {
Text(
text = comment.text,
style = MaterialTheme.typography.body1,
color = colorResource(id = R.color.text_primary),
lineHeight = 16.sp,
modifier = Modifier.padding(start = CommentDefaults.CommentContentStartPadding)
)
}
CommentReactions(
reactions = comment.reactions,
onReactionClick = currentOnReactionClick,
modifier = Modifier.padding(start = CommentDefaults.CommentContentStartPadding)
)
if (isShowRepliesBtnVisible) {
ShowRepliesButton {
onShowRepliesClick(comment.id)
}
}
}
}

@Composable
private fun ShowRepliesButton(
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
Box(
modifier = modifier
.clip(RoundedCornerShape(8.dp))
.clickable(onClick = onClick)
.padding(CommentDefaults.CommentContentStartPadding)
) {
Text(
text = stringResource(id = R.string.comments_show_replies_btn),
fontSize = 14.sp,
color = colorResource(id = R.color.color_primary_alpha_60)
)
}
}

@Preview
@Composable
private fun CommentPreview() {
HyperskillTheme {
Comment(
comment = CommentPreviewDataProvider.getSingleComment(),
isShowRepliesBtnVisible = true,
onReactionClick = { _, _ -> },
onShowRepliesClick = {}
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package org.hyperskill.app.android.comments.ui

import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.unit.dp

object CommentDefaults {
val CommentImageSize = 40.dp
val CommentImagePadding = 8.dp

val CommentContentVerticalPadding = 16.dp
val CommentContentStartPadding = 4.dp

val ReactionShape = RoundedCornerShape(8.dp)
val ReactionHorizontalPadding = 8.dp

private val CommentVerticalPadding = 24.dp
private val CommentHorizontalPadding = 20.dp

val RootCommentPadding = PaddingValues(
start = CommentHorizontalPadding,
end = CommentHorizontalPadding,
top = CommentVerticalPadding,
)

val ReplyCommentPadding = PaddingValues(
start = CommentHorizontalPadding + CommentImageSize + CommentImagePadding,
end = CommentHorizontalPadding,
top = CommentVerticalPadding
)

val SeparatorPadding = RootCommentPadding

val ShowMoreButtonPadding = PaddingValues(
horizontal = CommentHorizontalPadding,
vertical = CommentVerticalPadding
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package org.hyperskill.app.android.comments.ui

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import org.hyperskill.app.R
import org.hyperskill.app.android.core.view.ui.widget.compose.infiniteShimmer

@Composable
fun CommentHeader(
authorAvatar: String,
authorFullName: String,
formattedTime: String?,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.fillMaxWidth()
.height(IntrinsicSize.Min),
horizontalArrangement = Arrangement.spacedBy(CommentDefaults.CommentImagePadding)
) {
CommentAuthorAvatar(avatarUrl = authorAvatar)
Column(
modifier = Modifier.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween
) {
Text(
text = authorFullName,
style = MaterialTheme.typography.subtitle1,
fontSize = 16.sp,
color = colorResource(id = R.color.color_on_surface_alpha_60),
fontWeight = FontWeight.Bold,
lineHeight = 20.sp
)
if (formattedTime != null) {
Text(
text = formattedTime,
style = MaterialTheme.typography.body1,
fontSize = 14.sp,
color = colorResource(id = R.color.text_secondary),
lineHeight = 16.sp
)
}
}
}
}

@Composable
private fun CommentAuthorAvatar(
avatarUrl: String,
modifier: Modifier = Modifier
) {
SubcomposeAsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(avatarUrl)
.crossfade(true)
.build(),
modifier = modifier
.requiredSize(CommentDefaults.CommentImageSize)
.clip(CircleShape),
loading = {
CommentAuthorAvatarPlaceholder(playShimmer = true)
},
error = {
CommentAuthorAvatarPlaceholder(playShimmer = false)
},
contentDescription = null,
contentScale = ContentScale.FillWidth,
alignment = Alignment.Center,
filterQuality = FilterQuality.High
)
}

@Composable
private fun CommentAuthorAvatarPlaceholder(
playShimmer: Boolean,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.requiredSize(CommentDefaults.CommentImageSize)
.clip(CircleShape)
.background(colorResource(id = R.color.color_on_surface_alpha_12))
.infiniteShimmer(play = playShimmer)
)
}
Loading