diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/DomainServicesGraphQLClientConfig.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/DomainServicesGraphQLClientConfig.kt index e18927a7d7..2ef117c248 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/DomainServicesGraphQLClientConfig.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/DomainServicesGraphQLClientConfig.kt @@ -52,7 +52,8 @@ class RedwoodGraphQLClientConfig @Inject constructor( adapter: RedwoodAdapter ): DomainServicesGraphQLClientConfig( url = BuildConfig.REDWOOD_BASE_URL + "/graphql", - httpClient = adapter.buildOHttpClient() + httpClient = adapter.buildOHttpClient(), + fetchPolicy = HttpFetchPolicy.CacheFirst ) class JourneyGraphQLClientConfig @Inject constructor( diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonNotebookPage.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonNotebookPage.kt index 5269f5c680..f0b82550ff 100644 --- a/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonNotebookPage.kt +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/pages/HorizonNotebookPage.kt @@ -105,7 +105,7 @@ class HorizonNotebookPage(private val composeTestRule: ComposeTestRule) { } fun assertTextAreaPlaceholderDisplayed() { - composeTestRule.onNodeWithText(context.getString(R.string.addNoteAddANoteLabel)) + composeTestRule.onNodeWithText(context.getString(R.string.addNoteAddANoteOptionalLabel)) .assertIsDisplayed() } @@ -151,13 +151,13 @@ class HorizonNotebookPage(private val composeTestRule: ComposeTestRule) { } fun enterUserComment(comment: String) { - composeTestRule.onNodeWithText(context.getString(R.string.addNoteAddANoteLabel)) + composeTestRule.onNodeWithText(context.getString(R.string.addNoteAddANoteOptionalLabel)) .performClick() .performTextInput(comment) } fun clearUserComment() { - composeTestRule.onNode(hasText(context.getString(R.string.addNoteAddANoteLabel)).not()) + composeTestRule.onNode(hasText(context.getString(R.string.addNoteAddANoteOptionalLabel)).not()) .performClick() .performTextClearance() } @@ -194,7 +194,7 @@ class HorizonNotebookPage(private val composeTestRule: ComposeTestRule) { fun clearNoteText() { composeTestRule.onNode( - hasText(context.getString(R.string.addNoteAddANoteLabel)).not() + hasText(context.getString(R.string.addNoteAddANoteOptionalLabel)).not() ) .performClick() .performTextClearance() diff --git a/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/notebook/AddEditNoteScreenUiTest.kt b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/notebook/AddEditNoteScreenUiTest.kt index f78471d0b9..57dd8e26fc 100644 --- a/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/notebook/AddEditNoteScreenUiTest.kt +++ b/libs/horizon/src/androidTest/java/com/instructure/horizon/ui/features/notebook/AddEditNoteScreenUiTest.kt @@ -136,7 +136,7 @@ class AddEditNoteScreenUiTest { AddEditNoteScreen(navController, state) { _, _ -> } } - composeTestRule.onNodeWithText(context.getString(R.string.addNoteAddANoteLabel)) + composeTestRule.onNodeWithText(context.getString(R.string.addNoteAddANoteOptionalLabel)) .assertIsDisplayed() } diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardScreen.kt index b4f1809ca2..417edda725 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardScreen.kt @@ -231,7 +231,7 @@ private fun HomeScreenTopBar(uiState: DashboardUiState, mainNavController: NavCo ) Spacer(modifier = Modifier.weight(1f)) IconButton( - iconRes = R.drawable.menu_book_notebook, + iconRes = R.drawable.edit_note, contentDescription = stringResource(R.string.a11y_dashboardNotebookButtonContentDescription), onClick = { mainNavController.navigate(MainNavigationRoute.Notebook.route) diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/timespent/DashboardTimeSpentWidget.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/timespent/DashboardTimeSpentWidget.kt index ee15812d5d..9294290e4d 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/timespent/DashboardTimeSpentWidget.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/timespent/DashboardTimeSpentWidget.kt @@ -72,7 +72,7 @@ fun DashboardTimeSpentSection( } DashboardItemState.ERROR -> { DashboardWidgetCardError( - stringResource(R.string.dashboardTimeSpentTitle), + stringResource(R.string.dashboardTimeLearningTitle), R.drawable.schedule, HorizonColors.PrimitivesHoney.honey12(), false, diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/timespent/card/DashboardTimeSpentCardContent.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/timespent/card/DashboardTimeSpentCardContent.kt index 12dc48d1e1..0a2d3350a6 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/timespent/card/DashboardTimeSpentCardContent.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/widget/timespent/card/DashboardTimeSpentCardContent.kt @@ -62,7 +62,7 @@ fun DashboardTimeSpentCardContent( modifier: Modifier = Modifier, ) { DashboardWidgetCard( - stringResource(R.string.dashboardTimeSpentTitle), + stringResource(R.string.dashboardTimeLearningTitle), R.drawable.schedule, HorizonColors.PrimitivesHoney.honey12(), modifier, diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceScreen.kt index e681e4c029..dcc96a3c30 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceScreen.kt @@ -256,7 +256,9 @@ private fun ModuleItemSequenceContent( moduleHeaderHeight = coordinates.size.height val temp = nestedScrollConnection.appBarOffset nestedScrollConnection = - CollapsingAppBarNestedScrollConnection(moduleHeaderHeight).apply { appBarOffset = temp } + CollapsingAppBarNestedScrollConnection(moduleHeaderHeight).apply { + appBarOffset = temp + } } } ) { @@ -274,7 +276,10 @@ private fun ModuleItemSequenceContent( containerColor = Color.Transparent, modifier = Modifier .conditional(uiState.loadingState.isLoading || uiState.loadingState.isError) { - background(color = HorizonColors.Surface.pageSecondary(), shape = HorizonCornerRadius.level5) + background( + color = HorizonColors.Surface.pageSecondary(), + shape = HorizonCornerRadius.level5 + ) } .padding(top = moduleHeaderHeight) ) { @@ -305,6 +310,7 @@ private fun ModuleItemSequenceContent( uiState.assignmentToolsOpened, updateAiContext = uiState.updateAiAssistContext, updateNotebookContext = uiState.updateObjectTypeAndId, + scrollToNoteId = uiState.scrollToNoteId ) } } @@ -403,6 +409,7 @@ private fun ModuleItemContentScreen( assignmentToolsOpened: () -> Unit, updateAiContext: (AiAssistContextSource, String) -> Unit, updateNotebookContext: (Pair) -> Unit, + scrollToNoteId: String?, modifier: Modifier = Modifier ) { if (moduleItemUiState.isLoading) { @@ -461,7 +468,8 @@ private fun ModuleItemContentScreen( uiState = uiState, scrollState = scrollState, updateAiContext = { source, content -> updateAiContext(source, content) }, - mainNavController = mainNavController + mainNavController = mainNavController, + scrollToNoteId = scrollToNoteId ) } composable( @@ -546,6 +554,7 @@ private fun ModuleItemSequenceBottomBar( ) { if (showPreviousButton) IconButton( iconRes = R.drawable.chevron_left, + contentDescription = stringResource(R.string.a11y_previousModuleItem), color = IconButtonColor.Inverse, elevation = HorizonElevation.level4, onClick = onPreviousClick, @@ -559,13 +568,15 @@ private fun ModuleItemSequenceBottomBar( ) { IconButton( iconRes = R.drawable.ai, + contentDescription = stringResource(R.string.a11y_openAIAssistant), enabled = aiAssistEnabled, color = IconButtonColor.Ai, elevation = HorizonElevation.level4, onClick = onAiAssistClick, ) if (showNotebookButton) IconButton( - iconRes = R.drawable.menu_book_notebook, + iconRes = R.drawable.edit_note, + contentDescription = stringResource(R.string.a11y_openNotebook), enabled = notebookEnabled, color = IconButtonColor.Inverse, elevation = HorizonElevation.level4, @@ -573,6 +584,7 @@ private fun ModuleItemSequenceBottomBar( ) if (showAssignmentToolsButton) IconButton( iconRes = R.drawable.more_vert, + contentDescription = stringResource(R.string.a11y_openMoreOptions), color = IconButtonColor.Inverse, elevation = HorizonElevation.level4, onClick = onAssignmentToolsClick, @@ -587,6 +599,7 @@ private fun ModuleItemSequenceBottomBar( } if (showNextButton) IconButton( iconRes = R.drawable.chevron_right, + contentDescription = stringResource(R.string.a11y_nextModuleItem), color = IconButtonColor.Inverse, elevation = HorizonElevation.level4, onClick = onNextClick, diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceUiState.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceUiState.kt index e2fe68f661..88692f74cd 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceUiState.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceUiState.kt @@ -43,6 +43,7 @@ data class ModuleItemSequenceUiState( val hasUnreadComments: Boolean = false, val updateAiAssistContext: (AiAssistContextSource, String) -> Unit = { _, _ -> }, val shouldRefreshPreviousScreen: Boolean = false, + val scrollToNoteId: String? = null ) data class ModuleItemUiState( diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceViewModel.kt index a9189d1e99..a16f23c827 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceViewModel.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceViewModel.kt @@ -66,6 +66,7 @@ class ModuleItemSequenceViewModel @Inject constructor( private val moduleItemId = savedStateHandle.toRoute().moduleItemId private val moduleItemAssetType = savedStateHandle.toRoute().moduleItemAssetType private val moduleItemAssetId = savedStateHandle.toRoute().moduleItemAssetId + private val scrollToNoteId = savedStateHandle.toRoute().scrollToNoteId private var courseProgressChanged = false @@ -73,6 +74,7 @@ class ModuleItemSequenceViewModel @Inject constructor( MutableStateFlow( ModuleItemSequenceUiState( courseId = courseId, + scrollToNoteId = scrollToNoteId, loadingState = LoadingState(onRefresh = ::refresh), onPreviousClick = ::previousClicked, onNextClick = ::nextClicked, diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsContentScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsContentScreen.kt index e94aaf919d..b61aff4a4e 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsContentScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/content/page/PageDetailsContentScreen.kt @@ -30,6 +30,9 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedData +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedDataRange +import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedDataTextPosition import com.instructure.horizon.features.aiassistant.common.model.AiAssistContextSource import com.instructure.horizon.features.notebook.common.webview.ComposeNotesHighlightingCanvasWebView import com.instructure.horizon.features.notebook.common.webview.NotesCallback @@ -51,7 +54,8 @@ fun PageDetailsContentScreen( scrollState: ScrollState, updateAiContext: (AiAssistContextSource, String) -> Unit, mainNavController: NavHostController, - modifier: Modifier = Modifier + scrollToNoteId: String?, + modifier: Modifier = Modifier, ) { val activity = LocalContext.current.getActivityOrNull() LaunchedEffect(uiState.urlToOpen) { @@ -81,6 +85,7 @@ fun PageDetailsContentScreen( ComposeNotesHighlightingCanvasWebView( content = "
$it
", notes = uiState.notes, + scrollToNoteId = scrollToNoteId, applyOnWebView = { activity?.let { addVideoClient(it) } overrideHtmlFormatColors = HorizonColors.htmlFormatColors @@ -112,20 +117,21 @@ fun PageDetailsContentScreen( ) }, onNoteAdded = { selectedText, noteType, startContainer, startOffset, endContainer, endOffset, textSelectionStart, textSelectionEnd -> - mainNavController.navigate( - NotebookRoute.AddNotebook( - courseId = uiState.courseId.toString(), - objectType = "Page", - objectId = uiState.pageId.toString(), - highlightedTextStartOffset = startOffset, - highlightedTextEndOffset = endOffset, - highlightedTextStartContainer = startContainer, - highlightedTextEndContainer = endContainer, - highlightedText = selectedText, - textSelectionStart = textSelectionStart, - textSelectionEnd = textSelectionEnd, - noteType = noteType - ) + uiState.addNote( + NoteHighlightedData( + selectedText = selectedText, + range = NoteHighlightedDataRange( + startOffset = startOffset, + endOffset = endOffset, + startContainer = startContainer, + endContainer = endContainer + ), + textPosition = NoteHighlightedDataTextPosition( + start = textSelectionStart, + end = textSelectionEnd + ) + ), + noteType ) } ) diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/NotebookScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/NotebookScreen.kt index 4e03a3aafa..12fd22e197 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/NotebookScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/NotebookScreen.kt @@ -45,6 +45,7 @@ import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -216,6 +217,7 @@ fun NotebookScreen( courseId = note.courseId, moduleItemAssetType = note.objectType.value, moduleItemAssetId = note.objectId, + scrollToNoteId = note.id ) ) } @@ -303,7 +305,15 @@ private fun FilterContent( } if (state.showNoteTypeFilter) { - NotebookTypeSelect(state.selectedFilter, state.onFilterSelected, false, true) + NotebookTypeSelect( + state.selectedFilter, + state.onFilterSelected, + false, + true, + Modifier.conditional(!state.showCourseFilter) { // TalkBack hack to fix focus handling + semantics(true) {} + } + ) } } } @@ -333,7 +343,7 @@ private fun NoteContent( modifier = Modifier .fillMaxWidth() .horizonBorder( - colorResource(note.type.color).copy(alpha = 0.1f), + colorResource(note.type.highlightColor), 6.dp, 1.dp, 1.dp, diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/AddEditNoteScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/AddEditNoteScreen.kt index e2563e208e..e75460c77a 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/AddEditNoteScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/AddEditNoteScreen.kt @@ -17,6 +17,7 @@ package com.instructure.horizon.features.notebook.addedit import androidx.activity.compose.BackHandler +import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -33,10 +34,19 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.isTraversalGroup +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.traversalIndex import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -69,6 +79,7 @@ import com.instructure.horizon.horizonui.organisms.inputs.textarea.TextArea import com.instructure.horizon.horizonui.organisms.inputs.textarea.TextAreaState import com.instructure.pandautils.utils.ViewStyler import com.instructure.pandautils.utils.getActivityOrNull +import kotlinx.coroutines.delay @Composable fun AddEditNoteScreen( @@ -121,6 +132,15 @@ private fun AddEditNoteAppBar( state: AddEditNoteUiState, navigateBack: () -> Unit ) { + val requester = FocusRequester() + var requestFocus by rememberSaveable { mutableStateOf(true) } + LaunchedEffect(Unit) { + delay(50) + if(requestFocus) { + requester.requestFocus() + requestFocus = false + } + } CenterAlignedTopAppBar( colors = TopAppBarDefaults.topAppBarColors( containerColor = HorizonColors.Surface.pageSecondary(), @@ -131,7 +151,13 @@ private fun AddEditNoteAppBar( Text( state.title, style = HorizonTypography.h4, - color = HorizonColors.Text.title() + color = HorizonColors.Text.title(), + modifier = Modifier + .semantics { + traversalIndex = -1f + } + .focusRequester(requester) + .focusable() ) }, navigationIcon = { @@ -158,7 +184,12 @@ private fun AddEditNoteAppBar( enabled = state.hasContentChange && !state.isLoading ) }, - modifier = Modifier.padding(horizontal = 16.dp) + modifier = Modifier + .padding(horizontal = 16.dp) + .semantics { + isTraversalGroup = true + traversalIndex = -1f + } ) } @@ -174,7 +205,12 @@ private fun AddEditNoteContent(state: AddEditNoteUiState, padding: PaddingValues ) { HorizonSpace(SpaceSize.SPACE_24) - NotebookTypeSelect(state.type, state.onTypeChanged, true, false) + NotebookTypeSelect( + state.type, + state.onTypeChanged, + true, + false, + ) HorizonSpace(SpaceSize.SPACE_24) @@ -187,13 +223,12 @@ private fun AddEditNoteContent(state: AddEditNoteUiState, padding: PaddingValues TextArea( state = TextAreaState( - placeHolderText = stringResource(R.string.addNoteAddANoteLabel), + placeHolderText = stringResource(R.string.addNoteAddANoteOptionalLabel), required = InputLabelRequired.Optional, value = state.userComment, onValueChange = state.onUserCommentChanged, ), minLines = 5, - maxLines = 5 ) HorizonSpace(SpaceSize.SPACE_16) @@ -205,7 +240,7 @@ private fun AddEditNoteContent(state: AddEditNoteUiState, padding: PaddingValues if (state.lastModifiedDate != null) { Text( state.lastModifiedDate, - style = HorizonTypography.labelSmall, + style = HorizonTypography.labelMediumBold, color = HorizonColors.Text.timestamp(), modifier = Modifier.weight(1f), maxLines = 1, diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/add/AddNoteViewModel.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/add/AddNoteViewModel.kt index b3f20b07ac..d2f99ab75a 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/add/AddNoteViewModel.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/add/AddNoteViewModel.kt @@ -27,11 +27,13 @@ import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryLaunch import com.instructure.horizon.R import com.instructure.horizon.features.notebook.addedit.AddEditViewModel +import com.instructure.horizon.features.notebook.common.composable.toNotebookLocalisedDateFormat import com.instructure.horizon.features.notebook.common.model.NotebookType import com.instructure.horizon.features.notebook.navigation.NotebookRoute import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.update +import java.util.Date import javax.inject.Inject @HiltViewModel @@ -57,6 +59,7 @@ class AddNoteViewModel @Inject constructor( it.copy( title = context.getString(R.string.createNoteTitle), hasContentChange = true, + lastModifiedDate = Date().toNotebookLocalisedDateFormat(), highlightedData = NoteHighlightedData( selectedText = highlightedText, range = NoteHighlightedDataRange( diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/composable/NotebookHighlightedText.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/composable/NotebookHighlightedText.kt index 90561fef51..c45b51e940 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/composable/NotebookHighlightedText.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/composable/NotebookHighlightedText.kt @@ -42,7 +42,8 @@ fun NotebookHighlightedText( ) { var lineCount = 1 val lineList = mutableListOf() - val lineColor = type?.color?.let { colorResource(type.color) } + val highlightColor = type?.highlightColor?.let { colorResource(type.highlightColor) } + val lineColor = type?.lineColor?.let { colorResource(type.lineColor) } val dashedEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f) val pathEffect = if (type == NotebookType.Confusing) dashedEffect else null @@ -60,30 +61,38 @@ fun NotebookHighlightedText( }, modifier = modifier .drawWithContent { - drawContent() - if (lineColor != null) { + if (highlightColor != null && lineColor != null) { val strokeWidth = 1.dp.toPx() val lineHeight = size.height / lineCount for (i in 1..lineCount) { - val verticalOffset = i * lineHeight - strokeWidth + val verticalOffset = i * lineHeight val lineWidth = lineList[i - 1] drawLine( - color = lineColor, + color = highlightColor, + strokeWidth = lineHeight, + start = Offset(0f, verticalOffset - lineHeight / 2 + i * strokeWidth), + end = Offset(lineWidth, verticalOffset - lineHeight / 2 + i * strokeWidth) + ) + + drawLine( + color = highlightColor, strokeWidth = strokeWidth, - start = Offset(0f, verticalOffset), - end = Offset(lineWidth, verticalOffset), - pathEffect = pathEffect + start = Offset(0f, verticalOffset + i * strokeWidth + strokeWidth / 2), + end = Offset(lineWidth, verticalOffset + i * strokeWidth + strokeWidth / 2), + pathEffect = null ) drawLine( - color = lineColor.copy(alpha = 0.2f), - strokeWidth = lineHeight, - start = Offset(0f, verticalOffset - lineHeight / 2), - end = Offset(lineWidth, verticalOffset - lineHeight / 2) + color = lineColor, + strokeWidth = strokeWidth, + start = Offset(0f, verticalOffset + i * strokeWidth + strokeWidth / 2), + end = Offset(lineWidth, verticalOffset + i * strokeWidth + strokeWidth / 2), + pathEffect = pathEffect ) } } + drawContent() } ) } diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/composable/NotebookTypeSelect.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/composable/NotebookTypeSelect.kt index 12165e50b3..49b4039efd 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/composable/NotebookTypeSelect.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/composable/NotebookTypeSelect.kt @@ -62,7 +62,7 @@ fun NotebookTypeSelect( value = NotebookType.Important, label = context.getString(NotebookType.Important.labelRes), iconRes = NotebookType.Important.iconRes, - iconTint = Color(context.getColor(NotebookType.Important.color)), + iconTint = Color(context.getColor(NotebookType.Important.lineColor)), backgroundColor = importantBgColor ) ) @@ -71,7 +71,7 @@ fun NotebookTypeSelect( value = NotebookType.Confusing, label = context.getString(NotebookType.Confusing.labelRes), iconRes = NotebookType.Confusing.iconRes, - iconTint = Color(context.getColor(NotebookType.Confusing.color)), + iconTint = Color(context.getColor(NotebookType.Confusing.lineColor)), backgroundColor = confusingBgColor ) ) diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/model/NotebookType.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/model/NotebookType.kt index 73cecec714..56cd9d1b9a 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/model/NotebookType.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/model/NotebookType.kt @@ -8,8 +8,19 @@ import com.instructure.horizon.R enum class NotebookType( @StringRes val labelRes: Int, @DrawableRes val iconRes: Int, - @ColorRes val color: Int + @ColorRes val highlightColor: Int, + @ColorRes val lineColor: Int ) { - Confusing(R.string.notebookTypeUnclear, R.drawable.help, R.color.icon_error), - Important(R.string.notebookTypeImportant, R.drawable.keep_pin, R.color.icon_action), + Confusing( + R.string.notebookTypeUnclear, + R.drawable.help, + R.color.primitives_red12, + R.color.primitives_red57 + ), + Important( + R.string.notebookTypeImportant, + R.drawable.keep_pin, + R.color.primitives_sea12, + R.color.primitives_sea57 + ), } diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/webview/ComposeNotesHighlightingCanvasWebView.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/webview/ComposeNotesHighlightingCanvasWebView.kt index e468d6d953..73df3e387a 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/webview/ComposeNotesHighlightingCanvasWebView.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/webview/ComposeNotesHighlightingCanvasWebView.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue @@ -45,6 +46,7 @@ import com.instructure.horizon.features.notebook.common.model.Note import com.instructure.horizon.features.notebook.common.model.NotebookType import com.instructure.horizon.features.notebook.common.webview.JSTextSelectionInterface.Companion.addTextSelectionInterface import com.instructure.horizon.features.notebook.common.webview.JSTextSelectionInterface.Companion.evaluateTextSelectionInterface +import com.instructure.horizon.features.notebook.common.webview.JSTextSelectionInterface.Companion.getNoteYPosition import com.instructure.horizon.features.notebook.common.webview.JSTextSelectionInterface.Companion.highlightNotes import com.instructure.pandautils.compose.composables.ComposeEmbeddedWebViewCallbacks import com.instructure.pandautils.compose.composables.ComposeWebViewCallbacks @@ -52,6 +54,8 @@ import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.HtmlContentFormatter import com.instructure.pandautils.utils.JsExternalToolInterface import com.instructure.pandautils.utils.JsGoogleDocsInterface +import com.instructure.pandautils.utils.orDefault +import com.instructure.pandautils.utils.toPx import com.instructure.pandautils.views.CanvasWebView import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch @@ -69,7 +73,8 @@ fun ComposeNotesHighlightingCanvasWebView( applyOnWebView: (CanvasWebView.() -> Unit)? = null, webViewCallbacks: ComposeWebViewCallbacks = ComposeWebViewCallbacks(), embeddedWebViewCallbacks: ComposeEmbeddedWebViewCallbacks? = null, - scrollState: ScrollState? = null + scrollState: ScrollState? = null, + scrollToNoteId: String? = null ) { var pageHeight by remember { mutableIntStateOf(0) } var scrollValue by rememberSaveable { mutableIntStateOf(0) } @@ -78,6 +83,7 @@ fun ComposeNotesHighlightingCanvasWebView( val webViewState = rememberSaveable { bundleOf() } val selectionLocation: MutableStateFlow by remember { mutableStateOf(MutableStateFlow(SelectionLocation(0f, 0f, 0f, 0f))) } val lifecycleOwner = LocalLifecycleOwner.current + val composeScope = rememberCoroutineScope() val context = LocalContext.current val clipboardManager: ClipboardManager = LocalClipboardManager.current @@ -91,6 +97,10 @@ fun ComposeNotesHighlightingCanvasWebView( var selectedTextStart by remember { mutableIntStateOf(0) } var selectedTextEnd by remember { mutableIntStateOf(0) } + var isScrolled by rememberSaveable { mutableStateOf(false) } + var isPageLoaded by remember { mutableStateOf(false) } + var webViewInstance by remember { mutableStateOf(null) } + LaunchedEffect(scrollState?.maxValue) { val maxValue = scrollState?.maxValue ?: 0 @@ -100,6 +110,20 @@ fun ComposeNotesHighlightingCanvasWebView( } } + LaunchedEffect(scrollToNoteId, isPageLoaded, pageHeight) { + if (scrollToNoteId != null && isPageLoaded && scrollState != null && webViewInstance != null && !isScrolled && pageHeight > 0) { + isScrolled = true + webViewInstance?.webView?.getNoteYPosition(scrollToNoteId) { yPosition -> + if (yPosition != null && !scrollState.isScrollInProgress.orDefault(true)) { + composeScope.launch { + val targetScroll = (yPosition.toInt().toPx).coerceIn(0, scrollState.maxValue) + scrollState.animateScrollTo(targetScroll) + } + } + } + } + } + var previousHeight by remember { mutableIntStateOf(0) } if (LocalInspectionMode.current) { Text(text = content) @@ -107,7 +131,7 @@ fun ComposeNotesHighlightingCanvasWebView( AndroidView( factory = { scrollValue = scrollState?.value ?: 0 - NotesHighlightingCanvasWebViewWrapper( + val wrapper = NotesHighlightingCanvasWebViewWrapper( it, callback = AddNoteActionModeCallback( lifecycleOwner, @@ -160,7 +184,9 @@ fun ComposeNotesHighlightingCanvasWebView( webViewCallbacks.onPageFinished(webView, url) webView.evaluateTextSelectionInterface() - webView.highlightNotes(notesStateValue.value) + webView.highlightNotes(notesStateValue.value) { + isPageLoaded = true + } } override fun onPageStartedCallback(webView: WebView, url: String) = webViewCallbacks.onPageStarted(webView, url) @@ -184,6 +210,8 @@ fun ComposeNotesHighlightingCanvasWebView( applyOnWebView?.let { applyOnWebView -> webView.applyOnWebView() } } + webViewInstance = wrapper + wrapper }, update = { if (webViewState.isEmpty) { @@ -246,7 +274,9 @@ fun ComposeNotesHighlightingCanvasWebView( if (coordinates.size.height > 0 && coordinates.size.height != previousHeight) { lifecycleOwner.lifecycleScope.launch { val scrollRatio = scrollValue.toFloat() / previousScrollMaxValue.toFloat() - scrollState?.scrollTo((scrollRatio * (scrollState.maxValue)).toInt()) + if (scrollRatio > 0 && !scrollState?.isScrollInProgress.orDefault(true)) { + scrollState?.scrollTo((scrollRatio * (scrollState.maxValue)).toInt()) + } } } previousHeight = coordinates.size.height diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/webview/JSTextSelectionInterface.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/webview/JSTextSelectionInterface.kt index b1487d91cd..98cef76a4b 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/webview/JSTextSelectionInterface.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/webview/JSTextSelectionInterface.kt @@ -104,7 +104,8 @@ class JSTextSelectionInterface( val noteReactionString: String, val textSelectionStart: Int, val textSelectionEnd: Int, - val updatedAt: String + val updatedAt: String, + val accessibilityLabel: String ) companion object { @@ -652,7 +653,7 @@ const isNodeInRange = (range, node) => { ${JS_CODE_FROM_WEB} function highlightSelection(paramsJson) { const params = JSON.parse(paramsJson); - const { noteId, selectedText, userComment, startOffset, startContainer, endOffset, endContainer, noteReactionString, textSelectionStart, textSelectionEnd, updatedAt } = params; + const { noteId, selectedText, userComment, startOffset, startContainer, endOffset, endContainer, noteReactionString, textSelectionStart, textSelectionEnd, updatedAt, accessibilityLabel } = params; let parent = document.getElementById("parent-container");//document.documentElement; if (!parent) return; @@ -678,6 +679,9 @@ function highlightSelection(paramsJson) { const highlightElement = document.createElement("span"); highlightElement.classList.add(highlightClassName); highlightElement.classList.add(cssClass); + highlightElement.setAttribute('data-note-id', noteId); + highlightElement.setAttribute('role', 'button'); + highlightElement.setAttribute('aria-label', accessibilityLabel); highlightElement.onclick = function () { ${ JS_INTERFACE_NAME }.onHighlightedTextClicked(noteId, noteReactionString, selectedText, userComment, startOffset, startContainer, endOffset, endContainer, textSelectionStart, textSelectionEnd, updatedAt); }; highlightElement.textContent = textNode.textContent; @@ -687,6 +691,17 @@ function highlightSelection(paramsJson) { } } + +function getNotePosition(noteId) { + const highlights = document.getElementsByClassName('notebook-highlight'); + for (const highlight of highlights) { + if (highlight.getAttribute('data-note-id') === noteId) { + const rect = highlight.getBoundingClientRect(); + return rect.top + window.scrollY; + } + } + return 0; +} javascript: (function () { document.addEventListener("selectionchange", () => { const selection = window.getSelection(); @@ -750,8 +765,13 @@ javascript: (function () { this.evaluateJavascript(JS_CODE, null) } - fun WebView.highlightNotes(notes: List) { - notes.forEach { note -> + fun WebView.highlightNotes(notes: List, onHighlighted: () -> Unit) { + notes.forEachIndexed { index, note -> + val accessibilityLabel = context.getString( + com.instructure.horizon.R.string.a11y_notebookHighlightMarkedAs, + note.highlightedText.selectedText, + note.type.name + ) val params = HighlightParams( noteId = note.id, selectedText = note.highlightedText.selectedText, @@ -763,12 +783,23 @@ javascript: (function () { noteReactionString = note.type.name, textSelectionStart = note.highlightedText.textPosition.start, textSelectionEnd = note.highlightedText.textPosition.end, - updatedAt = note.updatedAt.toNotebookLocalisedDateFormat() + updatedAt = note.updatedAt.toNotebookLocalisedDateFormat(), + accessibilityLabel = accessibilityLabel ) val paramsJson = gson.toJson(params) val quotedJson = JSONObject.quote(paramsJson) val script = "javascript:highlightSelection($quotedJson)" - this.evaluateJavascript(script, null) + this.evaluateJavascript(script) { + if (index == notes.lastIndex){ onHighlighted() } + } + } + } + + fun WebView.getNoteYPosition(noteId: String, onResult: (Float?) -> Unit) { + val quotedNoteId = JSONObject.quote(noteId) + this.evaluateJavascript("getNotePosition($quotedNoteId)") { result -> + val position = result?.replace("\"", "")?.toFloatOrNull() + onResult(if (position != null && position >= 0) position else null) } } } diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/webview/NotesCallback.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/webview/NotesCallback.kt index 1575618422..30d8b44d45 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/webview/NotesCallback.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/common/webview/NotesCallback.kt @@ -32,7 +32,7 @@ data class NotesCallback( ) -> Unit, val onNoteAdded: ( selectedText: String, - noteType: String?, + noteType: String, startContainer: String, startOffset: Int, endContainer: String, diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/Modal.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/Modal.kt index 160c91134f..68bddb2c03 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/Modal.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/organisms/Modal.kt @@ -39,7 +39,6 @@ import com.instructure.canvasapi2.utils.ContextKeeper import com.instructure.horizon.R import com.instructure.horizon.horizonui.foundation.HorizonColors import com.instructure.horizon.horizonui.foundation.HorizonCornerRadius -import com.instructure.horizon.horizonui.foundation.HorizonElevation import com.instructure.horizon.horizonui.foundation.HorizonSpace import com.instructure.horizon.horizonui.foundation.HorizonTypography import com.instructure.horizon.horizonui.foundation.SpaceSize @@ -108,7 +107,6 @@ private fun DialogHeader( contentDescription = stringResource(R.string.a11y_close), size = IconButtonSize.SMALL, color = IconButtonColor.Inverse, - elevation = HorizonElevation.level4, onClick = onDismiss ) } diff --git a/libs/horizon/src/main/java/com/instructure/horizon/navigation/HorizonNavigation.kt b/libs/horizon/src/main/java/com/instructure/horizon/navigation/HorizonNavigation.kt index 7691c50df2..6c283d9f9f 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/navigation/HorizonNavigation.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/navigation/HorizonNavigation.kt @@ -67,7 +67,8 @@ sealed class MainNavigationRoute(val route: String) { val courseId: Long, val moduleItemId: Long? = null, val moduleItemAssetType: String? = null, - val moduleItemAssetId: String? = null + val moduleItemAssetId: String? = null, + val scrollToNoteId: String? = null ) : MainNavigationRoute("module_item_sequence") diff --git a/libs/horizon/src/main/java/com/instructure/horizon/navigation/HorizonNavigationTransitionRules.kt b/libs/horizon/src/main/java/com/instructure/horizon/navigation/HorizonNavigationTransitionRules.kt index 4e67353a42..303df8900f 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/navigation/HorizonNavigationTransitionRules.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/navigation/HorizonNavigationTransitionRules.kt @@ -45,6 +45,16 @@ val animationRules = listOf( to = NotebookRoute.Notebook.route, style = NavigationTransitionAnimation.SCALE ), + NavigationTransitionAnimationRule( + from = MainNavigationRoute.ModuleItemSequence.serializableRoute, + to = NotebookRoute.AddNotebook.serializableRoute, + style = NavigationTransitionAnimation.SCALE + ), + NavigationTransitionAnimationRule( + from = MainNavigationRoute.ModuleItemSequence.serializableRoute, + to = NotebookRoute.EditNotebook.serializableRoute, + style = NavigationTransitionAnimation.SCALE + ), //Account NavigationTransitionAnimationRule( diff --git a/libs/horizon/src/main/res/values/strings.xml b/libs/horizon/src/main/res/values/strings.xml index 8e87a2d57e..b1d91ae53c 100644 --- a/libs/horizon/src/main/res/values/strings.xml +++ b/libs/horizon/src/main/res/values/strings.xml @@ -258,7 +258,7 @@ This is where all your notes, taken directly within your learning objects, are stored and organized. It\'s your personal hub for keeping track of key insights, important excerpts, and reflections as you learn. Dive in to review or expand on your notes anytime! Highlight Label - Add a note + Add a note (optional) Save Delete File type is not allowed @@ -369,7 +369,7 @@ Please try again. Refresh Program details - Time learning + Time learning This widget will update once data becomes available. We weren\'t able to load this content.\nPlease try again. Refresh @@ -479,4 +479,10 @@ Exit Cancel Delete note + Previous module item + Open AI Assistant + Open notebook + Open more options + Next module item + %1$s, marked as %2$s, double tap to edit note \ No newline at end of file diff --git a/libs/horizon/src/test/java/com/instructure/horizon/features/notebook/addedit/add/AddNoteViewModelTest.kt b/libs/horizon/src/test/java/com/instructure/horizon/features/notebook/addedit/add/AddNoteViewModelTest.kt index 790f961c1d..7311845b8e 100644 --- a/libs/horizon/src/test/java/com/instructure/horizon/features/notebook/addedit/add/AddNoteViewModelTest.kt +++ b/libs/horizon/src/test/java/com/instructure/horizon/features/notebook/addedit/add/AddNoteViewModelTest.kt @@ -17,12 +17,10 @@ package com.instructure.horizon.features.notebook.addedit.add import android.content.Context +import android.text.format.DateFormat import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.SavedStateHandle import androidx.navigation.toRoute -import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedData -import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedDataRange -import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedDataTextPosition import com.instructure.horizon.R import com.instructure.horizon.features.notebook.common.model.NotebookType import com.instructure.horizon.features.notebook.navigation.NotebookRoute @@ -64,9 +62,11 @@ class AddNoteViewModelTest { fun setup() { Dispatchers.setMain(testDispatcher) mockkStatic("androidx.navigation.SavedStateHandleKt") + mockkStatic("android.text.format.DateFormat") every { context.getString(R.string.createNoteTitle) } returns "Add note" every { context.getString(R.string.noteHasBeenSavedMessage) } returns "Note has been saved" every { context.getString(R.string.failedToSaveNoteMessage) } returns "Failed to save note" + every { DateFormat.getBestDateTimePattern(any(), any()) } returns "yyyy-MM-dd HH:mm" setupSavedStateHandle( courseId = testCourseId,