From 1ef4282d446abcd21dcfac262f346774c5179b5e Mon Sep 17 00:00:00 2001 From: Akos Hermann Date: Thu, 27 Jun 2024 14:33:31 +0200 Subject: [PATCH 01/50] version bump --- apps/student/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/student/build.gradle b/apps/student/build.gradle index 376f4de73c..296a57e8c6 100644 --- a/apps/student/build.gradle +++ b/apps/student/build.gradle @@ -40,8 +40,8 @@ android { applicationId "com.instructure.candroid" minSdkVersion Versions.MIN_SDK targetSdkVersion Versions.TARGET_SDK - versionCode = 262 - versionName = '7.4.0' + versionCode = 263 + versionName = '7.5.0' vectorDrawables.useSupportLibrary = true multiDexEnabled = true From 04ec3236c422c501833b7f06aa10cc2447d3c1af Mon Sep 17 00:00:00 2001 From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com> Date: Mon, 1 Jul 2024 13:11:24 +0200 Subject: [PATCH 02/50] [MBL-17686][Student][Teacher] Full screen option does not display for embedded media #2489 refs: MBL-17686 affects: Student, Teacher release note: Fixed a bug where videos couldn't be opened in full screen. --- .../assignments/details/AssignmentDetailsFragment.kt | 1 + .../assignment/details/AssignmentDetailsFragment.kt | 8 ++++---- .../instructure/teacher/fragments/PageDetailsFragment.kt | 8 ++++---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsFragment.kt index d9cf249358..ca4608a833 100644 --- a/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsFragment.kt @@ -383,6 +383,7 @@ class AssignmentDetailsFragment : ParentFragment(), Bookmarkable { } private fun setupDescriptionView() { + binding?.descriptionWebViewWrapper?.webView?.addVideoClient(requireActivity()) binding?.descriptionWebViewWrapper?.webView?.canvasWebViewClientCallback = object : CanvasWebView.CanvasWebViewClientCallback { override fun openMediaFromWebView(mime: String, url: String, filename: String) { RouteMatcher.openMedia(requireActivity(), url) diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/details/AssignmentDetailsFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/details/AssignmentDetailsFragment.kt index d712f63cea..4efcd0bd57 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/details/AssignmentDetailsFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/details/AssignmentDetailsFragment.kt @@ -280,14 +280,14 @@ class AssignmentDetailsFragment : BasePresenterFragment< // Show progress bar while loading description descriptionProgressBar.announceForAccessibility(getString(R.string.loading)) descriptionProgressBar.setVisible() - descriptionWebViewWrapper.webView.setWebChromeClient(object : WebChromeClient() { - override fun onProgressChanged(view: WebView?, newProgress: Int) { - super.onProgressChanged(view, newProgress) + descriptionWebViewWrapper.webView.addVideoClient(requireActivity()) + descriptionWebViewWrapper.webView.canvasWebChromeClientCallback = object : CanvasWebView.CanvasWebChromeClientCallback { + override fun onProgressChangedCallback(view: WebView?, newProgress: Int) { if (newProgress >= 100) { descriptionProgressBar.setGone() } } - }) + } descriptionWebViewWrapper.webView.canvasWebViewClientCallback = object : CanvasWebView.CanvasWebViewClientCallback { override fun openMediaFromWebView(mime: String, url: String, filename: String) { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/PageDetailsFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/PageDetailsFragment.kt index 0d178e31b3..cbb2126a10 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/PageDetailsFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/PageDetailsFragment.kt @@ -151,14 +151,14 @@ class PageDetailsFragment : BasePresenterFragment< } } - canvasWebViewWraper.webView.webChromeClient = (object : WebChromeClient() { - override fun onProgressChanged(view: WebView?, newProgress: Int) { - super.onProgressChanged(view, newProgress) + canvasWebViewWraper.webView.addVideoClient(requireActivity()) + canvasWebViewWraper.webView.canvasWebChromeClientCallback = object : CanvasWebView.CanvasWebChromeClientCallback { + override fun onProgressChangedCallback(view: WebView?, newProgress: Int) { if (newProgress >= 100) { loading.setGone() } } - }) + } canvasWebViewWraper.webView.canvasEmbeddedWebViewCallback = object : CanvasWebView.CanvasEmbeddedWebViewCallback { override fun launchInternalWebViewFragment(url: String) = requireActivity().startActivity(InternalWebViewActivity.createIntent(requireActivity(), url, getString(R.string.utils_externalToolTitle), true)) From 992f6f228f27acc81c97577e30b9ad8bf955f952 Mon Sep 17 00:00:00 2001 From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com> Date: Thu, 4 Jul 2024 12:41:15 +0200 Subject: [PATCH 03/50] [MBL-17691][Student][Teacher] Recurrence edit does not consider the previous change in the calendar #2492 refs: MBL-17691 affects: Student, Teacher release note: Fixed an issue where editing a single occurrence in a recurring event would show an incorrect error message. --- .../canvasapi2/apis/CalendarEventAPI.kt | 8 ++ .../CreateUpdateEventRepository.kt | 89 ++++++++++--------- .../createupdate/CreateUpdateEventUiState.kt | 2 +- .../CreateUpdateEventViewModel.kt | 7 +- .../composables/CreateUpdateEventScreen.kt | 4 +- .../CreateUpdateEventRepositoryTest.kt | 43 +++++++-- 6 files changed, 101 insertions(+), 52 deletions(-) diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CalendarEventAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CalendarEventAPI.kt index b3bdbfa768..f32cae2689 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CalendarEventAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CalendarEventAPI.kt @@ -106,6 +106,14 @@ object CalendarEventAPI { @Tag restParams: RestParams ): DataResult> + @PUT("calendar_events/{eventId}") + suspend fun updateRecurringCalendarEventOneOccurrence( + @Path("eventId") eventId: Long, + @Query(value = "which") modifyEventScope: String, + @Body body: ScheduleItem.ScheduleItemParamsWrapper, + @Tag restParams: RestParams + ): DataResult + @PUT("calendar_events/{eventId}") suspend fun updateCalendarEvent( @Path("eventId") eventId: Long, diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendarevent/createupdate/CreateUpdateEventRepository.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendarevent/createupdate/CreateUpdateEventRepository.kt index be91030e5e..a8cb2e56f8 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendarevent/createupdate/CreateUpdateEventRepository.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendarevent/createupdate/CreateUpdateEventRepository.kt @@ -70,48 +70,55 @@ abstract class CreateUpdateEventRepository( locationName: String, locationAddress: String, description: String, - modifyEventScope: CalendarEventAPI.ModifyEventScope + modifyEventScope: CalendarEventAPI.ModifyEventScope, + isSeriesEvent: Boolean = false ): List { - if (modifyEventScope == CalendarEventAPI.ModifyEventScope.ONE && rrule.isEmpty()) { - val result = calendarEventApi.updateCalendarEvent( - eventId = eventId, - body = ScheduleItem.ScheduleItemParamsWrapper( - ScheduleItem.ScheduleItemParams( - contextCode = contextCode, - title = title, - description = description, - startAt = startDate, - endAt = endDate, - isAllDay = startDate == endDate, - rrule = rrule, - locationName = locationName, - locationAddress = locationAddress, - timeZoneEdited = TimeZone.getDefault().id - ) - ), - restParams = RestParams() - ).dataOrThrow - return listOf(result) - } else { - return calendarEventApi.updateRecurringCalendarEvent( - eventId = eventId, - modifyEventScope = modifyEventScope.apiName, - body = ScheduleItem.ScheduleItemParamsWrapper( - ScheduleItem.ScheduleItemParams( - contextCode = contextCode, - title = title, - description = description, - startAt = startDate, - endAt = endDate, - isAllDay = startDate == endDate, - rrule = rrule, - locationName = locationName, - locationAddress = locationAddress, - timeZoneEdited = TimeZone.getDefault().id - ) - ), - restParams = RestParams() - ).dataOrThrow + val body = ScheduleItem.ScheduleItemParamsWrapper( + ScheduleItem.ScheduleItemParams( + contextCode = contextCode, + title = title, + description = description, + startAt = startDate, + endAt = endDate, + isAllDay = startDate == endDate, + rrule = rrule, + locationName = locationName, + locationAddress = locationAddress, + timeZoneEdited = TimeZone.getDefault().id + ) + ) + + val result = when { + isSeriesEvent && modifyEventScope == CalendarEventAPI.ModifyEventScope.ONE -> { + listOf( + calendarEventApi.updateRecurringCalendarEventOneOccurrence( + eventId = eventId, + modifyEventScope = modifyEventScope.apiName, + body = body, + restParams = RestParams() + ).dataOrThrow + ) + } + + rrule.isEmpty() -> { + listOf( + calendarEventApi.updateCalendarEvent( + eventId = eventId, + body = body, + restParams = RestParams() + ).dataOrThrow + ) + } + + else -> { + calendarEventApi.updateRecurringCalendarEvent( + eventId = eventId, + modifyEventScope = modifyEventScope.apiName, + body = body, + restParams = RestParams() + ).dataOrThrow + } } + return result } } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendarevent/createupdate/CreateUpdateEventUiState.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendarevent/createupdate/CreateUpdateEventUiState.kt index ca1a4c3d70..f83d5790b5 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendarevent/createupdate/CreateUpdateEventUiState.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendarevent/createupdate/CreateUpdateEventUiState.kt @@ -83,7 +83,7 @@ sealed class CreateUpdateEventAction { data class UpdateLocation(val location: String) : CreateUpdateEventAction() data class UpdateAddress(val address: String) : CreateUpdateEventAction() data class UpdateDetails(val details: String) : CreateUpdateEventAction() - data class Save(val modifyEventScope: CalendarEventAPI.ModifyEventScope) : CreateUpdateEventAction() + data class Save(val modifyEventScope: CalendarEventAPI.ModifyEventScope, val isSeriesEvent: Boolean = false) : CreateUpdateEventAction() data object SnackbarDismissed : CreateUpdateEventAction() data object ShowSelectCalendarScreen : CreateUpdateEventAction() data object HideSelectCalendarScreen : CreateUpdateEventAction() diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendarevent/createupdate/CreateUpdateEventViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendarevent/createupdate/CreateUpdateEventViewModel.kt index 8a2004ebf5..75a0b8c7f6 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendarevent/createupdate/CreateUpdateEventViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendarevent/createupdate/CreateUpdateEventViewModel.kt @@ -137,7 +137,7 @@ class CreateUpdateEventViewModel @Inject constructor( _uiState.update { it.copy(details = action.details) } } - is CreateUpdateEventAction.Save -> save(action.modifyEventScope) + is CreateUpdateEventAction.Save -> save(action.modifyEventScope, action.isSeriesEvent) is CreateUpdateEventAction.SnackbarDismissed -> { _uiState.update { it.copy(errorSnack = null) } @@ -461,7 +461,7 @@ class CreateUpdateEventViewModel @Inject constructor( } } - private fun save(modifyEventScope: CalendarEventAPI.ModifyEventScope) = with(uiState.value) { + private fun save(modifyEventScope: CalendarEventAPI.ModifyEventScope, isSeriesEvent: Boolean) = with(uiState.value) { _uiState.update { it.copy(saving = true) } viewModelScope.tryLaunch { val startDate = LocalDateTime.of(date, startTime ?: LocalTime.of(6, 0)).toApiString().orEmpty() @@ -480,7 +480,8 @@ class CreateUpdateEventViewModel @Inject constructor( locationName = location, locationAddress = address, description = details, - modifyEventScope = modifyEventScope + modifyEventScope = modifyEventScope, + isSeriesEvent = isSeriesEvent ).also { CanvasRestAdapter.clearCacheUrls("calendar_events/") } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendarevent/createupdate/composables/CreateUpdateEventScreen.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendarevent/createupdate/composables/CreateUpdateEventScreen.kt index d5bb1782aa..78813bd1d0 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendarevent/createupdate/composables/CreateUpdateEventScreen.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendarevent/createupdate/composables/CreateUpdateEventScreen.kt @@ -262,7 +262,7 @@ private fun ActionsSegment( }, onConfirmation = { showModifyScopeDialog = false - actionHandler(CreateUpdateEventAction.Save(CalendarEventAPI.ModifyEventScope.entries[it])) + actionHandler(CreateUpdateEventAction.Save(CalendarEventAPI.ModifyEventScope.entries[it], true)) } ) } @@ -275,7 +275,7 @@ private fun ActionsSegment( if (uiState.isSeriesEvent) { showModifyScopeDialog = true } else { - actionHandler(CreateUpdateEventAction.Save(CalendarEventAPI.ModifyEventScope.ONE)) + actionHandler(CreateUpdateEventAction.Save(CalendarEventAPI.ModifyEventScope.ONE, false)) } }, enabled = saveEnabled, diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/calendarevent/createupdate/CreateUpdateEventRepositoryTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/calendarevent/createupdate/CreateUpdateEventRepositoryTest.kt index 6e3680ad0e..271afcc8f9 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/calendarevent/createupdate/CreateUpdateEventRepositoryTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/calendarevent/createupdate/CreateUpdateEventRepositoryTest.kt @@ -138,7 +138,7 @@ class CreateUpdateEventRepositoryTest { } @Test - fun `Update single calendar event successful`() = runTest { + fun `Update single calendar event when rrule is empty and event is not recurring`() = runTest { val event = ScheduleItem("itemId") coEvery { @@ -159,7 +159,8 @@ class CreateUpdateEventRepositoryTest { locationName = "locationName", locationAddress = "locationAddress", description = "description", - CalendarEventAPI.ModifyEventScope.ONE + CalendarEventAPI.ModifyEventScope.ONE, + isSeriesEvent = false ) Assert.assertEquals(listOf(event), result) @@ -188,7 +189,8 @@ class CreateUpdateEventRepositoryTest { locationName = "locationName", locationAddress = "locationAddress", description = "description", - CalendarEventAPI.ModifyEventScope.ONE + CalendarEventAPI.ModifyEventScope.ONE, + isSeriesEvent = false ) Assert.assertEquals(events, result) @@ -220,7 +222,7 @@ class CreateUpdateEventRepositoryTest { } @Test - fun `Update recurring calendar event successful`() = runTest { + fun `Update recurring calendar event successful when updating all events`() = runTest { val event = listOf(ScheduleItem("itemId")) coEvery { @@ -242,9 +244,40 @@ class CreateUpdateEventRepositoryTest { locationName = "locationName", locationAddress = "locationAddress", description = "description", - CalendarEventAPI.ModifyEventScope.ALL + CalendarEventAPI.ModifyEventScope.ALL, + isSeriesEvent = true ) Assert.assertEquals(event, result) } + + @Test + fun `Update recurring calendar event calls only one occurrence api call when only one occurrence is edited`() = runTest { + val event = ScheduleItem("itemId") + + coEvery { + calendarEventApi.updateRecurringCalendarEventOneOccurrence( + any(), + any(), + any(), + any() + ) + } returns DataResult.Success(event) + + val result = repository.updateEvent( + 1L, + title = "title", + startDate = "startDate", + endDate = "endDate", + rrule = "rrule", + contextCode = "contextCode", + locationName = "locationName", + locationAddress = "locationAddress", + description = "description", + CalendarEventAPI.ModifyEventScope.ONE, + isSeriesEvent = true + ) + + Assert.assertEquals(listOf(event), result) + } } From 6c5dcf4daaa4902d0917479a04119b08e2508804 Mon Sep 17 00:00:00 2001 From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com> Date: Thu, 4 Jul 2024 12:41:50 +0200 Subject: [PATCH 04/50] [MBL-17630][Student][Teacher] Calendar Redesign Focus Management Issues when selecting Calendar #2490 refs: MBL-17630 affects: Student, Teacher release note: none --- .../student/activity/NavigationActivity.kt | 4 ++++ .../features/calendar/CalendarScreenTest.kt | 8 ++++++++ .../features/calendar/CalendarFragment.kt | 10 +++++++++- .../calendar/composables/CalendarScreen.kt | 20 +++++++++++++++++-- 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt index 3eb89bfd8b..0da611c2bf 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt @@ -995,6 +995,10 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. } ft.show(fragment) ft.commitAllowingStateLoss() + + if (fragment is CalendarFragment) { + fragment.calendarTabSelected() + } } private fun getBottomBarFragments(selectedFragmentName: String): List { diff --git a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/calendar/CalendarScreenTest.kt b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/calendar/CalendarScreenTest.kt index 42e8b48650..7576ad9770 100644 --- a/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/calendar/CalendarScreenTest.kt +++ b/libs/pandautils/src/androidTest/java/com/instructure/pandautils/compose/features/calendar/CalendarScreenTest.kt @@ -65,6 +65,7 @@ class CalendarScreenTest { ), stateMapper.createBodyUiState(true, LocalDate.now()) ) ), + false, actionHandler = {}, navigationActionClick = {}, ) @@ -98,6 +99,7 @@ class CalendarScreenTest { ), stateMapper.createBodyUiState(true, selectedDate) ) ), + false, actionHandler = {}, navigationActionClick = {}, ) @@ -133,6 +135,7 @@ class CalendarScreenTest { ), stateMapper.createBodyUiState(true, selectedDate) ) ), + false, actionHandler = { actions.add(it) }, navigationActionClick = {}, ) @@ -160,6 +163,7 @@ class CalendarScreenTest { ), stateMapper.createBodyUiState(true, selectedDate) ) ), + false, actionHandler = {}, navigationActionClick = {}, ) @@ -188,6 +192,7 @@ class CalendarScreenTest { ), stateMapper.createBodyUiState(true, selectedDate) ) ), + false, actionHandler = {}, navigationActionClick = {}, ) @@ -220,6 +225,7 @@ class CalendarScreenTest { ), stateMapper.createBodyUiState(true, selectedDate) ) ), + false, actionHandler = { actions.add(it) }, navigationActionClick = {}, ) @@ -252,6 +258,7 @@ class CalendarScreenTest { ), stateMapper.createBodyUiState(true, selectedDate) ) ), + false, actionHandler = { actions.add(it) }, navigationActionClick = {}, ) @@ -282,6 +289,7 @@ class CalendarScreenTest { ), stateMapper.createBodyUiState(true, selectedDate) ), snackbarMessage = "Snackbar message" ), + false, actionHandler = {}, navigationActionClick = {}, ) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendar/CalendarFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendar/CalendarFragment.kt index ac76750b2c..cbac032e2b 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendar/CalendarFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendar/CalendarFragment.kt @@ -24,6 +24,7 @@ import android.view.ViewGroup import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels @@ -58,6 +59,9 @@ class CalendarFragment : Fragment(), NavigationCallbacks, FragmentInteractions { @Inject lateinit var calendarRouter: CalendarRouter + // This is needed to trigger accessibility focus on the calendar screen when the tab is selected + private var triggerCalendarScreenAccessibilityFocus = mutableStateOf(false) + @OptIn(ExperimentalFoundationApi::class) override fun onCreateView( inflater: LayoutInflater, @@ -72,13 +76,17 @@ class CalendarFragment : Fragment(), NavigationCallbacks, FragmentInteractions { setContent { val uiState by viewModel.uiState.collectAsState() val actionHandler = { action: CalendarAction -> viewModel.handleAction(action) } - CalendarScreen(title(), uiState, actionHandler) { + CalendarScreen(title(), uiState, triggerCalendarScreenAccessibilityFocus.value, actionHandler) { calendarRouter.openNavigationDrawer() } } } } + fun calendarTabSelected() { + triggerCalendarScreenAccessibilityFocus.value = !triggerCalendarScreenAccessibilityFocus.value + } + private fun handleAction(action: CalendarViewModelAction) { when (action) { is CalendarViewModelAction.OpenAssignment -> calendarRouter.openAssignment(action.canvasContext, action.assignmentId) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendar/composables/CalendarScreen.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendar/composables/CalendarScreen.kt index 670a1461b4..9f5050e85b 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendar/composables/CalendarScreen.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/calendar/composables/CalendarScreen.kt @@ -17,6 +17,7 @@ package com.instructure.pandautils.features.calendar.composables import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable +import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -37,6 +38,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope 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.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag @@ -65,6 +68,7 @@ import com.instructure.pandautils.features.calendar.CalendarUiState import com.instructure.pandautils.features.calendar.EventUiState import com.instructure.pandautils.utils.ThemePrefs import com.jakewharton.threetenabp.AndroidThreeTen +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.threeten.bp.Clock import org.threeten.bp.LocalDate @@ -74,9 +78,12 @@ import org.threeten.bp.LocalDate fun CalendarScreen( title: String, calendarScreenUiState: CalendarScreenUiState, + triggerAccessibilityFocus: Boolean, actionHandler: (CalendarAction) -> Unit, navigationActionClick: () -> Unit ) { + val focusRequester = remember { FocusRequester() } + CanvasTheme { val snackbarHostState = remember { SnackbarHostState() } val localCoroutineScope = rememberCoroutineScope() @@ -121,7 +128,10 @@ fun CalendarScreen( }, navigationActionClick = navigationActionClick, navIconRes = R.drawable.ic_hamburger, - navIconContentDescription = stringResource(id = R.string.navigation_drawer_open) + navIconContentDescription = stringResource(id = R.string.navigation_drawer_open), + modifier = Modifier + .focusable() + .focusRequester(focusRequester) ) }, snackbarHost = { SnackbarHost(hostState = snackbarHostState, modifier = Modifier.testTag("snackbarHost")) }, @@ -180,6 +190,12 @@ fun CalendarScreen( } ) } + + // This is needed to trigger accessibility focus on the calendar screen when the tab is selected + LaunchedEffect(key1 = triggerAccessibilityFocus, block = { + delay(1000) + focusRequester.requestFocus() + }) } @ExperimentalFoundationApi @@ -219,5 +235,5 @@ fun CalendarScreenPreview() { ) ) ) - ), {}) {} + ), false, {}) {} } \ No newline at end of file From 4f96b352141055a125508519a0bbadfc358040c6 Mon Sep 17 00:00:00 2001 From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com> Date: Thu, 4 Jul 2024 14:42:22 +0200 Subject: [PATCH 05/50] [MBL-17670][Parent] Navigation changes & not-a-parent screen refs: MBL-17670 affects: Parent release note: none --- .../ui/compose/NotAParentScreenTest.kt | 79 ++++++ .../interaction/DashboardInteractionTest.kt | 126 ++++++++++ .../interaction/NotAParentInteractionsTest.kt | 120 ++++++++++ .../parentapp/ui/pages/DashboardPage.kt | 78 ++++++ .../parentapp/ui/pages/NotAParentPage.kt | 38 +++ .../instructure/parentapp/utils/ParentTest.kt | 3 + .../parentapp/utils/ParentTestExtensions.kt | 8 +- apps/parent/src/main/AndroidManifest.xml | 9 +- .../parentapp/di/ApplicationModule.kt | 9 + .../di/{MainModule.kt => DashboardModule.kt} | 20 +- .../instructure/parentapp/di/SplashModule.kt | 42 ++++ .../features/alerts/list/AlertsFragment.kt | 15 +- .../features/calendar/CalendarFragment.kt | 15 +- .../courses/details/CourseDetailsFragment.kt | 15 +- .../features/courses/list/CoursesFragment.kt | 9 +- .../features/courses/list/CoursesScreen.kt | 7 +- .../features/courses/list/CoursesUiState.kt | 2 +- .../features/courses/list/CoursesViewModel.kt | 10 +- .../features/dashboard/DashboardFragment.kt | 226 ++++++++++++++++++ .../features/dashboard/DashboardRepository.kt | 40 ++++ .../DashboardViewData.kt} | 9 +- .../DashboardViewModel.kt} | 58 ++--- .../SelectedStudentHolder.kt | 4 +- .../StudentItemViewModel.kt | 2 +- .../parentapp/features/main/MainActivity.kt | 170 ++----------- .../managestudents/ManageStudentsFragment.kt | 44 ++++ .../features/notaparent/NotAParentFragment.kt | 66 +++++ .../features/notaparent/NotAParentScreen.kt | 196 +++++++++++++++ .../features/settings/SettingsFragment.kt | 42 ++++ .../parentapp/features/splash/SplashAction.kt | 25 ++ .../features/splash/SplashFragment.kt | 101 ++++++++ .../SplashRepository.kt} | 28 +-- .../features/splash/SplashViewModel.kt | 84 +++++++ .../parentapp/util/navigation/Navigation.kt | 117 +++++++++ .../src/main/res/layout/activity_main.xml | 166 +------------ .../src/main/res/layout/fragment_alerts.xml | 21 -- .../src/main/res/layout/fragment_calendar.xml | 21 -- .../res/layout/fragment_course_details.xml | 21 -- .../main/res/layout/fragment_dashboard.xml | 176 ++++++++++++++ .../src/main/res/layout/item_student.xml | 2 +- .../navigation_drawer_header_layout.xml | 2 +- apps/parent/src/main/res/menu/bottom_nav.xml | 6 +- .../parent/src/main/res/navigation/alerts.xml | 30 --- .../src/main/res/navigation/calendar.xml | 30 --- .../src/main/res/navigation/courses.xml | 45 ---- .../src/main/res/navigation/nav_graph.xml | 35 --- .../courses/list/CoursesViewModelTest.kt | 10 +- .../dashboard/DashboardRepositoryTest.kt | 80 +++++++ .../DashboardViewModelTest.kt} | 73 +----- .../TestSelectStudentHolder.kt | 2 +- .../SplashRepositoryTest.kt} | 24 +- .../features/splash/SplashViewModelTest.kt | 173 ++++++++++++++ libs/pandares/src/main/res/values/strings.xml | 25 +- .../compose/composables/EmptyContent.kt | 31 ++- .../pandautils/utils/ViewExtensions.kt | 115 +-------- .../pandautils/utils/ViewStyler.kt | 15 +- 56 files changed, 2070 insertions(+), 850 deletions(-) create mode 100644 apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/NotAParentScreenTest.kt create mode 100644 apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/DashboardInteractionTest.kt create mode 100644 apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/NotAParentInteractionsTest.kt create mode 100644 apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/DashboardPage.kt create mode 100644 apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/NotAParentPage.kt rename apps/parent/src/main/java/com/instructure/parentapp/di/{MainModule.kt => DashboardModule.kt} (69%) create mode 100644 apps/parent/src/main/java/com/instructure/parentapp/di/SplashModule.kt create mode 100644 apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardFragment.kt create mode 100644 apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardRepository.kt rename apps/parent/src/main/java/com/instructure/parentapp/features/{main/MainViewData.kt => dashboard/DashboardViewData.kt} (85%) rename apps/parent/src/main/java/com/instructure/parentapp/features/{main/MainViewModel.kt => dashboard/DashboardViewModel.kt} (78%) rename apps/parent/src/main/java/com/instructure/parentapp/features/{main => dashboard}/SelectedStudentHolder.kt (90%) rename apps/parent/src/main/java/com/instructure/parentapp/features/{main => dashboard}/StudentItemViewModel.kt (95%) create mode 100644 apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentsFragment.kt create mode 100644 apps/parent/src/main/java/com/instructure/parentapp/features/notaparent/NotAParentFragment.kt create mode 100644 apps/parent/src/main/java/com/instructure/parentapp/features/notaparent/NotAParentScreen.kt create mode 100644 apps/parent/src/main/java/com/instructure/parentapp/features/settings/SettingsFragment.kt create mode 100644 apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashAction.kt create mode 100644 apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashFragment.kt rename apps/parent/src/main/java/com/instructure/parentapp/features/{main/MainRepository.kt => splash/SplashRepository.kt} (88%) create mode 100644 apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashViewModel.kt create mode 100644 apps/parent/src/main/java/com/instructure/parentapp/util/navigation/Navigation.kt delete mode 100644 apps/parent/src/main/res/layout/fragment_alerts.xml delete mode 100644 apps/parent/src/main/res/layout/fragment_calendar.xml delete mode 100644 apps/parent/src/main/res/layout/fragment_course_details.xml create mode 100644 apps/parent/src/main/res/layout/fragment_dashboard.xml delete mode 100644 apps/parent/src/main/res/navigation/alerts.xml delete mode 100644 apps/parent/src/main/res/navigation/calendar.xml delete mode 100644 apps/parent/src/main/res/navigation/courses.xml delete mode 100644 apps/parent/src/main/res/navigation/nav_graph.xml create mode 100644 apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/DashboardRepositoryTest.kt rename apps/parent/src/test/java/com/instructure/parentapp/features/{main/MainViewModelTest.kt => dashboard/DashboardViewModelTest.kt} (72%) rename apps/parent/src/test/java/com/instructure/parentapp/features/{main => dashboard}/TestSelectStudentHolder.kt (94%) rename apps/parent/src/test/java/com/instructure/parentapp/features/{main/MainRepositoryTest.kt => splash/SplashRepositoryTest.kt} (84%) create mode 100644 apps/parent/src/test/java/com/instructure/parentapp/features/splash/SplashViewModelTest.kt diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/NotAParentScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/NotAParentScreenTest.kt new file mode 100644 index 0000000000..70de846d41 --- /dev/null +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/NotAParentScreenTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.parentapp.ui.compose + +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.parentapp.features.notaparent.NotAParentScreen +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + + +@RunWith(AndroidJUnit4::class) +class NotAParentScreenTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun assertContent() { + composeTestRule.setContent { + NotAParentScreen( + returnToLoginClick = {}, + onStudentClick = {}, + onTeacherClick = {} + ) + } + + composeTestRule.onNodeWithText("Not a parent?").assertIsDisplayed() + composeTestRule.onNodeWithText("We couldn't find any students associated with your account").assertIsDisplayed() + composeTestRule.onNodeWithText("Return to login") + .assertIsDisplayed() + .assertHasClickAction() + composeTestRule.onNodeWithText("Are you a student or teacher?") + .assertIsDisplayed() + .assertHasClickAction() + composeTestRule.onNodeWithText("STUDENT") + .assertIsNotDisplayed() + } + + @Test + fun assertAppOptions() { + composeTestRule.setContent { + NotAParentScreen( + returnToLoginClick = {}, + onStudentClick = {}, + onTeacherClick = {} + ) + } + + composeTestRule.onNodeWithText("Are you a student or teacher?").performClick() + composeTestRule.onNodeWithText("STUDENT") + .assertIsDisplayed() + .assertHasClickAction() + composeTestRule.onNodeWithText("TEACHER") + .assertIsDisplayed() + .assertHasClickAction() + } +} diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/DashboardInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/DashboardInteractionTest.kt new file mode 100644 index 0000000000..264ce4131e --- /dev/null +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/DashboardInteractionTest.kt @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.parentapp.ui.interaction + +import androidx.compose.ui.platform.ComposeView +import androidx.test.espresso.assertion.ViewAssertions +import androidx.test.espresso.matcher.ViewMatchers +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils +import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck +import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.waitForMatcherWithSleeps +import com.instructure.loginapi.login.R +import com.instructure.parentapp.utils.ParentTest +import com.instructure.parentapp.utils.tokenLogin +import dagger.hilt.android.testing.HiltAndroidTest +import org.hamcrest.Matchers +import org.junit.Test + + +@HiltAndroidTest +class DashboardInteractionTest : ParentTest() { + + @Test + fun testObserverData() { + val data = initData() + + goToDashboard(data) + + dashboardPage.openNavigationDrawer() + dashboardPage.assertObserverData(data.parents.first()) + } + + @Test + fun testChangeStudent() { + val data = initData() + + goToDashboard(data) + + val students = data.students.sortedBy { it.sortableName } + dashboardPage.assertSelectedStudent(students.first().shortName!!) + dashboardPage.openStudentSelector() + dashboardPage.selectStudent(data.students.last().shortName!!) + dashboardPage.assertSelectedStudent(students.last().shortName!!) + } + + @Test + fun testLogout() { + val data = initData() + + goToDashboard(data) + + dashboardPage.openNavigationDrawer() + dashboardPage.tapLogout() + dashboardPage.assertLogoutDialog() + dashboardPage.tapOk() + waitForMatcherWithSleeps(ViewMatchers.withId(R.id.canvasLogo), 20000).check( + ViewAssertions.matches( + ViewMatchers.isDisplayed() + ) + ) + } + + @Test + fun testSwitchUsers() { + val data = initData() + + goToDashboard(data) + + dashboardPage.openNavigationDrawer() + dashboardPage.tapSwitchUsers() + waitForMatcherWithSleeps(ViewMatchers.withId(R.id.canvasLogo), 20000).check( + ViewAssertions.matches( + ViewMatchers.isDisplayed() + ) + ) + } + + private fun initData(): MockCanvas { + return MockCanvas.init( + parentCount = 1, + studentCount = 2, + courseCount = 1 + ) + } + + private fun goToDashboard(data: MockCanvas) { + val parent = data.parents.first() + val token = data.tokenFor(parent)!! + tokenLogin(data.domain, token, parent) + } + + override fun displaysPageObjects() = Unit + + override fun enableAndConfigureAccessibilityChecks() { + extraAccessibilitySupressions = Matchers.allOf( + AccessibilityCheckResultUtils.matchesCheck( + SpeakableTextPresentCheck::class.java + ), + AccessibilityCheckResultUtils.matchesViews( + ViewMatchers.withParent( + ViewMatchers.withClassName( + Matchers.equalTo(ComposeView::class.java.name) + ) + ) + ) + ) + + super.enableAndConfigureAccessibilityChecks() + } +} diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/NotAParentInteractionsTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/NotAParentInteractionsTest.kt new file mode 100644 index 0000000000..0bccedad38 --- /dev/null +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/NotAParentInteractionsTest.kt @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.parentapp.ui.interaction + +import android.content.Intent +import androidx.test.espresso.assertion.ViewAssertions +import androidx.test.espresso.intent.Intents +import androidx.test.espresso.intent.matcher.IntentMatchers +import androidx.test.espresso.matcher.ViewMatchers +import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockCanvas.addCourse +import com.instructure.canvas.espresso.mockCanvas.addEnrollment +import com.instructure.canvas.espresso.mockCanvas.addUser +import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvas.espresso.mockCanvas.updateUserEnrollments +import com.instructure.canvas.espresso.waitForMatcherWithSleeps +import com.instructure.canvasapi2.models.Enrollment +import com.instructure.loginapi.login.R +import com.instructure.parentapp.ui.pages.NotAParentPage +import com.instructure.parentapp.utils.ParentComposeTest +import com.instructure.parentapp.utils.tokenLogin +import dagger.hilt.android.testing.HiltAndroidTest +import org.hamcrest.CoreMatchers +import org.junit.Test + + +@HiltAndroidTest +class NotAParentInteractionsTest : ParentComposeTest() { + + private val notAParentPage = NotAParentPage(composeTestRule) + + @Test + fun testLogout() { + val data = initData() + goToNotAParentScreen(data) + + notAParentPage.tapReturnToLogin() + waitForMatcherWithSleeps(ViewMatchers.withId(R.id.canvasLogo), 20000).check( + ViewAssertions.matches( + ViewMatchers.isDisplayed() + ) + ) + } + + @Test + fun testTapStudent() { + val data = initData() + goToNotAParentScreen(data) + + notAParentPage.expandAppOptions() + Intents.init() + try { + val expectedIntent = CoreMatchers.allOf( + IntentMatchers.hasAction(Intent.ACTION_VIEW), + CoreMatchers.anyOf( + // Could be either of these, depending on whether the play store app is installed + IntentMatchers.hasData("market://details?id=com.instructure.candroid"), + IntentMatchers.hasData("https://play.google.com/store/apps/details?id=com.instructure.candroid") + ) + ) + notAParentPage.tapApp("STUDENT") + Intents.intended(expectedIntent) + } finally { + Intents.release() + } + } + + @Test + fun testTapTeacher() { + val data = initData() + goToNotAParentScreen(data) + + notAParentPage.expandAppOptions() + Intents.init() + try { + val expectedIntent = CoreMatchers.allOf( + IntentMatchers.hasAction(Intent.ACTION_VIEW), + CoreMatchers.anyOf( + // Could be either of these, depending on whether the play store app is installed + IntentMatchers.hasData("market://details?id=com.instructure.teacher"), + IntentMatchers.hasData("https://play.google.com/store/apps/details?id=com.instructure.teacher") + ) + ) + notAParentPage.tapApp("TEACHER") + Intents.intended(expectedIntent) + } finally { + Intents.release() + } + } + + private fun initData(): MockCanvas { + val data = MockCanvas.init() + val parent = data.addUser() + val course = data.addCourse() + data.addEnrollment(parent, course, Enrollment.EnrollmentType.Observer) + data.updateUserEnrollments() + return data + } + + private fun goToNotAParentScreen(data: MockCanvas) { + val parent = data.parents.first() + val token = data.tokenFor(parent)!! + tokenLogin(data.domain, token, parent, false) + } +} diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/DashboardPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/DashboardPage.kt new file mode 100644 index 0000000000..37ba572767 --- /dev/null +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/DashboardPage.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.parentapp.ui.pages + +import com.instructure.canvasapi2.models.User +import com.instructure.espresso.OnViewWithId +import com.instructure.espresso.assertDisplayed +import com.instructure.espresso.click +import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.getStringFromResource +import com.instructure.espresso.page.onView +import com.instructure.espresso.page.onViewWithContentDescription +import com.instructure.espresso.page.onViewWithText +import com.instructure.espresso.page.plus +import com.instructure.espresso.page.withAncestor +import com.instructure.espresso.page.withText +import com.instructure.parentapp.R +import org.hamcrest.Matchers.equalToIgnoringCase + +class DashboardPage : BasePage(R.id.drawer_layout) { + + private val toolbar by OnViewWithId(R.id.toolbar) + private val bottomNavigationView by OnViewWithId(R.id.bottom_nav) + + fun assertObserverData(user: User) { + onViewWithText(user.name).assertDisplayed() + onViewWithText(user.email.orEmpty()).assertDisplayed() + } + + fun openNavigationDrawer() { + onViewWithContentDescription(R.string.navigation_drawer_open).click() + } + + fun assertSelectedStudent(name: String) { + onView(withText(name) + withAncestor(R.id.selected_student_container)).assertDisplayed() + } + + fun openStudentSelector() { + toolbar.click() + } + + fun selectStudent(name: String) { + onView(withText(name) + withAncestor(R.id.student_list)).click() + } + + fun tapLogout() { + onViewWithText(R.string.logout).click() + } + + fun assertLogoutDialog() { + onViewWithText(R.string.logout_warning).assertDisplayed() + onViewWithText(equalToIgnoringCase(getStringFromResource(android.R.string.cancel))).assertDisplayed() + onViewWithText(equalToIgnoringCase(getStringFromResource(android.R.string.ok))).assertDisplayed() + } + + fun tapOk() { + onViewWithText(android.R.string.ok).click() + } + + fun tapSwitchUsers() { + onViewWithText(R.string.navigationDrawerSwitchUsers).click() + } +} diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/NotAParentPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/NotAParentPage.kt new file mode 100644 index 0000000000..72ba16cb88 --- /dev/null +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/NotAParentPage.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.parentapp.ui.pages + +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick + + +class NotAParentPage(private val composeTestRule: ComposeTestRule) { + + fun expandAppOptions() { + composeTestRule.onNodeWithText("Are you a student or teacher?").performClick() + } + + fun tapReturnToLogin() { + composeTestRule.onNodeWithText("Return to login").performClick() + } + + fun tapApp(appName: String) { + composeTestRule.onNodeWithText(appName).performClick() + } +} diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTest.kt index 852cf6a357..1d1a2b3d2f 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTest.kt @@ -20,6 +20,7 @@ package com.instructure.parentapp.utils import com.instructure.canvas.espresso.CanvasTest import com.instructure.parentapp.BuildConfig import com.instructure.parentapp.features.login.LoginActivity +import com.instructure.parentapp.ui.pages.DashboardPage abstract class ParentTest : CanvasTest() { @@ -27,4 +28,6 @@ abstract class ParentTest : CanvasTest() { override val isTesting = BuildConfig.IS_TESTING override val activityRule = ParentActivityTestRule(LoginActivity::class.java) + + val dashboardPage = DashboardPage() } \ No newline at end of file diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTestExtensions.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTestExtensions.kt index ca51ca25b8..27d32e3fe5 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTestExtensions.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTestExtensions.kt @@ -17,15 +17,11 @@ package com.instructure.parentapp.utils -import androidx.test.espresso.assertion.ViewAssertions -import androidx.test.espresso.matcher.ViewMatchers -import com.instructure.canvas.espresso.waitForMatcherWithSleeps import com.instructure.canvasapi2.models.User -import com.instructure.parentapp.R import com.instructure.parentapp.features.login.LoginActivity -fun ParentTest.tokenLogin(domain: String, token: String, user: User) { +fun ParentTest.tokenLogin(domain: String, token: String, user: User, assertDashboard: Boolean = true) { activityRule.runOnUiThread { (originalActivity as LoginActivity).loginWithToken( token, @@ -34,5 +30,5 @@ fun ParentTest.tokenLogin(domain: String, token: String, user: User) { ) } - waitForMatcherWithSleeps(ViewMatchers.withId(R.id.toolbar), 20000).check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + if (assertDashboard) dashboardPage.assertPageObjects() } diff --git a/apps/parent/src/main/AndroidManifest.xml b/apps/parent/src/main/AndroidManifest.xml index 2596ab4229..14ab33d460 100644 --- a/apps/parent/src/main/AndroidManifest.xml +++ b/apps/parent/src/main/AndroidManifest.xml @@ -20,8 +20,8 @@ + android:maxSdkVersion="29" + tools:replace="android:maxSdkVersion" /> @@ -120,9 +121,7 @@ android:exported="false" android:label="@string/canvas" android:launchMode="singleTask" - android:theme="@style/CanvasMaterialTheme_Default"> - - + android:theme="@style/CanvasMaterialTheme_Default" /> \ No newline at end of file diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/ApplicationModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/ApplicationModule.kt index cf556d6c79..7d0f341cf6 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/di/ApplicationModule.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/di/ApplicationModule.kt @@ -18,15 +18,18 @@ package com.instructure.parentapp.di import com.instructure.canvasapi2.utils.Analytics +import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.loginapi.login.util.PreviousUsersUtils import com.instructure.loginapi.login.util.QRLogin import com.instructure.pandautils.utils.LogoutHelper import com.instructure.parentapp.util.ParentLogoutHelper import com.instructure.parentapp.util.ParentPrefs +import com.instructure.parentapp.util.navigation.Navigation import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) @@ -56,4 +59,10 @@ class ApplicationModule { fun provideParentPrefs(): ParentPrefs { return ParentPrefs } + + @Provides + @Singleton + fun provideNavigation(apiPrefs: ApiPrefs): Navigation { + return Navigation(apiPrefs) + } } \ No newline at end of file diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/MainModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/DashboardModule.kt similarity index 69% rename from apps/parent/src/main/java/com/instructure/parentapp/di/MainModule.kt rename to apps/parent/src/main/java/com/instructure/parentapp/di/DashboardModule.kt index 1a922e07a5..b3bdc2ab5c 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/di/MainModule.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/di/DashboardModule.kt @@ -18,11 +18,9 @@ package com.instructure.parentapp.di import com.instructure.canvasapi2.apis.EnrollmentAPI -import com.instructure.canvasapi2.apis.ThemeAPI -import com.instructure.canvasapi2.apis.UserAPI -import com.instructure.parentapp.features.main.MainRepository -import com.instructure.parentapp.features.main.SelectedStudentHolder -import com.instructure.parentapp.features.main.SelectedStudentHolderImpl +import com.instructure.parentapp.features.dashboard.DashboardRepository +import com.instructure.parentapp.features.dashboard.SelectedStudentHolder +import com.instructure.parentapp.features.dashboard.SelectedStudentHolderImpl import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -32,15 +30,13 @@ import javax.inject.Singleton @Module @InstallIn(ViewModelComponent::class) -class MainModule { +class DashboardModule { @Provides - fun provideMainRepository( - enrollmentApi: EnrollmentAPI.EnrollmentInterface, - userApi: UserAPI.UsersInterface, - themeApi: ThemeAPI.ThemeInterface - ): MainRepository { - return MainRepository(enrollmentApi, userApi, themeApi) + fun provideDashboardRepository( + enrollmentApi: EnrollmentAPI.EnrollmentInterface + ): DashboardRepository { + return DashboardRepository(enrollmentApi) } } diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/SplashModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/SplashModule.kt new file mode 100644 index 0000000000..e49db22b04 --- /dev/null +++ b/apps/parent/src/main/java/com/instructure/parentapp/di/SplashModule.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.parentapp.di + +import com.instructure.canvasapi2.apis.EnrollmentAPI +import com.instructure.canvasapi2.apis.ThemeAPI +import com.instructure.canvasapi2.apis.UserAPI +import com.instructure.parentapp.features.splash.SplashRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent + + +@Module +@InstallIn(ViewModelComponent::class) +class SplashModule { + + @Provides + fun provideSplashRepository( + userApi: UserAPI.UsersInterface, + themeApi: ThemeAPI.ThemeInterface, + enrollmentApi: EnrollmentAPI.EnrollmentInterface + ): SplashRepository { + return SplashRepository(userApi, themeApi, enrollmentApi) + } +} \ No newline at end of file diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsFragment.kt index d34b8534cc..e75f2afa7c 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsFragment.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsFragment.kt @@ -21,13 +21,22 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.material.Text +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment -import com.instructure.parentapp.R class AlertsFragment : Fragment() { - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return layoutInflater.inflate(R.layout.fragment_alerts, container, false) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return ComposeView(requireActivity()).apply { + setContent { + Text(text = "Alerts") + } + } } } \ No newline at end of file diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/CalendarFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/CalendarFragment.kt index b4dcf79cfc..2cb88c4ab4 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/CalendarFragment.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/calendar/CalendarFragment.kt @@ -21,13 +21,22 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.material.Text +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment -import com.instructure.parentapp.R class CalendarFragment : Fragment() { - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return layoutInflater.inflate(R.layout.fragment_calendar, container, false) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return ComposeView(requireActivity()).apply { + setContent { + Text(text = "Calendar") + } + } } } \ No newline at end of file diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsFragment.kt index 8d9d0cd059..5435b31b82 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsFragment.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/details/CourseDetailsFragment.kt @@ -21,13 +21,22 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.material.Text +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment -import com.instructure.parentapp.R class CourseDetailsFragment : Fragment() { - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return layoutInflater.inflate(R.layout.fragment_course_details, container, false) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return ComposeView(requireActivity()).apply { + setContent { + Text(text = "Course Details") + } + } } } \ No newline at end of file diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/list/CoursesFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/list/CoursesFragment.kt index ac3b46d61f..1fd6b2e8a0 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/list/CoursesFragment.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/list/CoursesFragment.kt @@ -17,7 +17,6 @@ package com.instructure.parentapp.features.courses.list -import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -28,9 +27,10 @@ import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope -import androidx.navigation.fragment.findNavController import com.instructure.pandautils.utils.collectOneOffEvents +import com.instructure.parentapp.util.navigation.Navigation import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject @AndroidEntryPoint @@ -38,6 +38,9 @@ class CoursesFragment : Fragment() { private val viewModel: CoursesViewModel by viewModels() + @Inject + lateinit var navigation: Navigation + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -56,7 +59,7 @@ class CoursesFragment : Fragment() { private fun handleAction(action: CoursesViewModelAction) { when (action) { is CoursesViewModelAction.NavigateToCourseDetails -> { - findNavController().navigate(Uri.parse(action.navigationUrl)) + navigation.navigate(activity, navigation.courseDetailsRoute(action.courseId)) } } } diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/list/CoursesScreen.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/list/CoursesScreen.kt index cdc695edec..fecbb39972 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/list/CoursesScreen.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/list/CoursesScreen.kt @@ -18,11 +18,10 @@ package com.instructure.parentapp.features.courses.list 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.PaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -105,8 +104,6 @@ private fun CourseListContent( modifier = modifier.pullRefresh(pullRefreshState) ) { LazyColumn( - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxSize() ) { items(uiState.courseListItems) { @@ -134,10 +131,12 @@ private fun CourseListItem( ) { Column( modifier = modifier + .fillMaxWidth() .testTag("courseListItem") .clickable { actionHandler(CoursesAction.CourseTapped(uiState.courseId)) } + .padding(horizontal = 16.dp, vertical = 8.dp) ) { Text( text = uiState.courseName, diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/list/CoursesUiState.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/list/CoursesUiState.kt index 491e0e59bf..aa49968991 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/list/CoursesUiState.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/list/CoursesUiState.kt @@ -26,5 +26,5 @@ sealed class CoursesAction { } sealed class CoursesViewModelAction { - data class NavigateToCourseDetails(val navigationUrl: String) : CoursesViewModelAction() + data class NavigateToCourseDetails(val courseId: Long) : CoursesViewModelAction() } diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/list/CoursesViewModel.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/list/CoursesViewModel.kt index 8dce7ec86b..3d19e39021 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/courses/list/CoursesViewModel.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/courses/list/CoursesViewModel.kt @@ -20,11 +20,10 @@ package com.instructure.parentapp.features.courses.list import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.instructure.canvasapi2.models.User -import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryLaunch import com.instructure.pandautils.utils.ColorKeeper -import com.instructure.parentapp.features.main.SelectedStudentHolder +import com.instructure.parentapp.features.dashboard.SelectedStudentHolder import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow @@ -39,7 +38,6 @@ import javax.inject.Inject class CoursesViewModel @Inject constructor( private val repository: CoursesRepository, private val colorKeeper: ColorKeeper, - private val apiPrefs: ApiPrefs, private val selectedStudentHolder: SelectedStudentHolder, private val courseGradeFormatter: CourseGradeFormatter ) : ViewModel() { @@ -115,11 +113,7 @@ class CoursesViewModel @Inject constructor( when (action) { is CoursesAction.CourseTapped -> { viewModelScope.launch { - _events.send( - CoursesViewModelAction.NavigateToCourseDetails( - "${apiPrefs.fullDomain}/courses/${action.courseId}" - ) - ) + _events.send(CoursesViewModelAction.NavigateToCourseDetails(action.courseId)) } } diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardFragment.kt new file mode 100644 index 0000000000..bd812c3ecb --- /dev/null +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardFragment.kt @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.parentapp.features.dashboard + +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.core.os.BundleCompat +import androidx.core.view.GravityCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.navigation.NavController +import androidx.navigation.NavController.Companion.KEY_DEEP_LINK_INTENT +import androidx.navigation.fragment.NavHostFragment +import com.instructure.canvasapi2.models.User +import com.instructure.loginapi.login.tasks.LogoutTask +import com.instructure.pandautils.interfaces.NavigationCallbacks +import com.instructure.pandautils.utils.ColorKeeper +import com.instructure.pandautils.utils.ViewStyler +import com.instructure.pandautils.utils.animateCircularBackgroundColorChange +import com.instructure.pandautils.utils.applyTheme +import com.instructure.pandautils.utils.showThemed +import com.instructure.parentapp.R +import com.instructure.parentapp.databinding.FragmentDashboardBinding +import com.instructure.parentapp.databinding.NavigationDrawerHeaderLayoutBinding +import com.instructure.parentapp.util.ParentLogoutTask +import com.instructure.parentapp.util.ParentPrefs +import com.instructure.parentapp.util.navigation.Navigation +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import javax.inject.Inject + + +@AndroidEntryPoint +class DashboardFragment : Fragment(), NavigationCallbacks { + + private lateinit var binding: FragmentDashboardBinding + + private val viewModel: DashboardViewModel by viewModels() + + @Inject + lateinit var navigation: Navigation + + private lateinit var navController: NavController + private lateinit var headerLayoutBinding: NavigationDrawerHeaderLayoutBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentDashboardBinding.inflate(inflater, container, false) + binding.viewModel = viewModel + binding.lifecycleOwner = viewLifecycleOwner + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupNavigation() + + lifecycleScope.launch { + viewModel.data.collectLatest { + setupNavigationDrawerHeader(it.userViewData) + setupAppColors(it.selectedStudent) + } + } + + handleDeeplink() + } + + private fun setupNavigation() { + val navHostFragment = childFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment + navController = navHostFragment.navController + navController.graph = navigation.createDashboardNavGraph(navController) + + setupToolbar() + setupNavigationDrawer() + setupBottomNavigationView() + } + + private fun handleDeeplink() { + try { + val uri = BundleCompat.getParcelable( + arguments ?: return, + KEY_DEEP_LINK_INTENT, + Intent::class.java + )?.data + + uri?.let { + navController.navigate(it) + } + } catch (e: Exception) { + Log.e(this.javaClass.simpleName, e.message.orEmpty()) + } + } + + private fun setupToolbar() { + val toolbar = binding.toolbar + toolbar.setNavigationIcon(R.drawable.ic_hamburger) + toolbar.navigationContentDescription = getString(R.string.navigation_drawer_open) + toolbar.setNavigationOnClickListener { + openNavigationDrawer() + } + } + + private fun setupNavigationDrawer() { + val navView = binding.navView + + headerLayoutBinding = NavigationDrawerHeaderLayoutBinding.bind(navView.getHeaderView(0)) + + navView.setNavigationItemSelectedListener { + closeNavigationDrawer() + when (it.itemId) { + R.id.inbox -> menuItemSelected { navigation.navigate(activity, navigation.inbox) } + R.id.manage_students -> menuItemSelected { navigation.navigate(activity, navigation.manageStudents) } + R.id.settings -> menuItemSelected { navigation.navigate(activity, navigation.settings) } + R.id.help -> menuItemSelected { navigation.navigate(activity, navigation.help) } + R.id.log_out -> menuItemSelected { onLogout() } + R.id.switch_users -> menuItemSelected { onSwitchUsers() } + else -> false + } + } + } + + private fun menuItemSelected(action: () -> Unit): Boolean { + action() + return true + } + + private fun setupBottomNavigationView() { + val bottomNavigationView = binding.bottomNav + + bottomNavigationView.setOnItemSelectedListener { + when (it.itemId) { + R.id.courses -> navigateWithPopBackStack(navigation.courses) + R.id.calendar -> navigateWithPopBackStack(navigation.calendar) + R.id.alerts -> navigateWithPopBackStack(navigation.alerts) + else -> false + } + } + + navController.addOnDestinationChangedListener { _, destination, _ -> + val menuId = when (destination.route) { + navigation.courses -> R.id.courses + navigation.calendar -> R.id.calendar + navigation.alerts -> R.id.alerts + else -> return@addOnDestinationChangedListener + } + bottomNavigationView.menu.findItem(menuId).isChecked = true + } + } + + private fun navigateWithPopBackStack(route: String): Boolean { + navController.popBackStack() + navController.navigate(route) + return true + } + + private fun setupAppColors(student: User?) { + val color = ColorKeeper.getOrGenerateUserColor(student).backgroundColor() + if (binding.toolbar.background == null) { + binding.toolbar.setBackgroundColor(color) + } else { + binding.toolbar.animateCircularBackgroundColorChange(color, binding.toolbarImage) + } + binding.bottomNav.applyTheme(color, requireActivity().getColor(R.color.textDarkest)) + ViewStyler.setStatusBarDark(requireActivity(), color) + } + + private fun openNavigationDrawer() { + binding.drawerLayout.openDrawer(GravityCompat.START) + } + + private fun closeNavigationDrawer() { + binding.drawerLayout.closeDrawer(GravityCompat.START) + } + + private fun setupNavigationDrawerHeader(userViewData: UserViewData?) { + headerLayoutBinding.userViewData = userViewData + } + + private fun onLogout() { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.logout_warning) + .setPositiveButton(android.R.string.ok) { _, _ -> + ParentLogoutTask(LogoutTask.Type.LOGOUT).execute() + } + .setNegativeButton(android.R.string.cancel, null) + .showThemed(ColorKeeper.getOrGenerateUserColor(ParentPrefs.currentStudent).textAndIconColor()) + } + + private fun onSwitchUsers() { + ParentLogoutTask(LogoutTask.Type.SWITCH_USERS).execute() + } + + override fun onHandleBackPressed(): Boolean { + if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) { + closeNavigationDrawer() + return true + } + return false + } +} diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardRepository.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardRepository.kt new file mode 100644 index 0000000000..cba4e8fc2a --- /dev/null +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardRepository.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.parentapp.features.dashboard + +import com.instructure.canvasapi2.apis.EnrollmentAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.depaginate + + +class DashboardRepository( + private val enrollmentApi: EnrollmentAPI.EnrollmentInterface +) { + + suspend fun getStudents(): List { + val params = RestParams(usePerPageQueryParam = true) + return enrollmentApi.firstPageObserveeEnrollmentsParent(params).depaginate { + enrollmentApi.getNextPage(it, params) + }.dataOrNull + .orEmpty() + .mapNotNull { it.observedUser } + .distinct() + .sortedBy { it.sortableName } + } +} diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/main/MainViewData.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewData.kt similarity index 85% rename from apps/parent/src/main/java/com/instructure/parentapp/features/main/MainViewData.kt rename to apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewData.kt index 3b66e40718..24d6464cc8 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/main/MainViewData.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewData.kt @@ -15,23 +15,18 @@ * */ -package com.instructure.parentapp.features.main +package com.instructure.parentapp.features.dashboard import com.instructure.canvasapi2.models.User -data class MainViewData( +data class DashboardViewData( val userViewData: UserViewData? = null, val studentSelectorExpanded: Boolean = false, val studentItems: List = emptyList(), val selectedStudent: User? = null ) -sealed class MainAction { - data class ShowToast(val message: String) : MainAction() - data object LocaleChanged : MainAction() -} - data class StudentItemViewData( val studentId: Long, val studentName: String, diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/main/MainViewModel.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewModel.kt similarity index 78% rename from apps/parent/src/main/java/com/instructure/parentapp/features/main/MainViewModel.kt rename to apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewModel.kt index 7dc2550f9f..3c45498294 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/main/MainViewModel.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewModel.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.features.main +package com.instructure.parentapp.features.dashboard import android.content.Context import androidx.lifecycle.ViewModel @@ -26,38 +26,29 @@ import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryLaunch import com.instructure.loginapi.login.util.PreviousUsersUtils import com.instructure.pandautils.mvvm.ViewState -import com.instructure.pandautils.utils.ColorKeeper -import com.instructure.pandautils.utils.ThemePrefs import com.instructure.pandautils.utils.orDefault import com.instructure.parentapp.R import com.instructure.parentapp.util.ParentPrefs import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class MainViewModel @Inject constructor( +class DashboardViewModel @Inject constructor( @ApplicationContext private val context: Context, - private val repository: MainRepository, + private val repository: DashboardRepository, private val previousUsersUtils: PreviousUsersUtils, private val apiPrefs: ApiPrefs, private val parentPrefs: ParentPrefs, - private val colorKeeper: ColorKeeper, - private val themePrefs: ThemePrefs, private val selectedStudentHolder: SelectedStudentHolder ) : ViewModel() { - private val _events = Channel() - val events = _events.receiveAsFlow() - - private val _data = MutableStateFlow(MainViewData()) + private val _data = MutableStateFlow(DashboardViewData()) val data = _data.asStateFlow() private val _state = MutableStateFlow(ViewState.Loading) @@ -66,23 +57,14 @@ class MainViewModel @Inject constructor( private val currentUser = previousUsersUtils.getSignedInUser(context, apiPrefs.domain, apiPrefs.user?.id.orDefault()) init { - loadInitialData() + loadData() } - private fun loadInitialData() { + private fun loadData() { viewModelScope.tryLaunch { _state.value = ViewState.Loading - val user = repository.getSelf() - user?.let { saveUserInfo(it) } - - val colors = repository.getColors() - colors?.let { colorKeeper.addToCache(it) } - - val theme = repository.getTheme() - theme?.let { themePrefs.applyCanvasTheme(it, context) } - - loadUserInfo() + setupUserInfo() loadStudents() @@ -102,15 +84,7 @@ class MainViewModel @Inject constructor( } } - private suspend fun saveUserInfo(user: User) { - val oldLocale = apiPrefs.effectiveLocale - apiPrefs.user = user - if (apiPrefs.effectiveLocale != oldLocale) { - _events.send(MainAction.LocaleChanged) - } - } - - private fun loadUserInfo() { + private fun setupUserInfo() { apiPrefs.user?.let { user -> _data.update { it.copy( @@ -150,6 +124,16 @@ class MainViewModel @Inject constructor( selectedStudent = selectedStudent ) } + + if (_data.value.studentItems.isEmpty()) { + _state.value = ViewState.Empty( + R.string.noStudentsError, + R.string.noStudentsErrorDescription, + R.drawable.panda_manage_students + ) + } else { + _state.value = ViewState.Success + } } private fun onStudentSelected(student: User) { @@ -173,10 +157,4 @@ class MainViewModel @Inject constructor( it.copy(studentSelectorExpanded = !it.studentSelectorExpanded) } } - - fun closeStudentSelector() { - _data.update { - it.copy(studentSelectorExpanded = false) - } - } } diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/main/SelectedStudentHolder.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/SelectedStudentHolder.kt similarity index 90% rename from apps/parent/src/main/java/com/instructure/parentapp/features/main/SelectedStudentHolder.kt rename to apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/SelectedStudentHolder.kt index 3c36032a07..15d965979d 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/main/SelectedStudentHolder.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/SelectedStudentHolder.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.features.main +package com.instructure.parentapp.features.dashboard import com.instructure.canvasapi2.models.User @@ -29,7 +29,7 @@ interface SelectedStudentHolder { } class SelectedStudentHolderImpl : SelectedStudentHolder { - private val _selectedStudentFlow = MutableSharedFlow() + private val _selectedStudentFlow = MutableSharedFlow(replay = 1) override val selectedStudentFlow = _selectedStudentFlow.asSharedFlow() override suspend fun updateSelectedStudent(user: User) { diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/main/StudentItemViewModel.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/StudentItemViewModel.kt similarity index 95% rename from apps/parent/src/main/java/com/instructure/parentapp/features/main/StudentItemViewModel.kt rename to apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/StudentItemViewModel.kt index 844e5a5b67..05f750c17a 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/main/StudentItemViewModel.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/StudentItemViewModel.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.features.main +package com.instructure.parentapp.features.dashboard import com.instructure.pandautils.mvvm.ItemViewModel import com.instructure.parentapp.R diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/main/MainActivity.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/main/MainActivity.kt index 2187a8eff1..803ea8fcef 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/main/MainActivity.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/main/MainActivity.kt @@ -21,193 +21,69 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle -import android.widget.Toast -import androidx.activity.viewModels -import androidx.appcompat.app.AlertDialog +import android.util.Log import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.GravityCompat -import androidx.drawerlayout.widget.DrawerLayout -import androidx.lifecycle.lifecycleScope import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment -import androidx.navigation.ui.AppBarConfiguration -import androidx.navigation.ui.navigateUp -import androidx.navigation.ui.setupWithNavController -import com.instructure.canvasapi2.models.User -import com.instructure.canvasapi2.utils.LocaleUtils -import com.instructure.loginapi.login.tasks.LogoutTask import com.instructure.pandautils.binding.viewBinding import com.instructure.pandautils.interfaces.NavigationCallbacks -import com.instructure.pandautils.utils.ColorKeeper import com.instructure.pandautils.utils.Const -import com.instructure.pandautils.utils.ViewStyler -import com.instructure.pandautils.utils.animateCircularBackgroundColorChange -import com.instructure.pandautils.utils.applyTheme -import com.instructure.pandautils.utils.collapse -import com.instructure.pandautils.utils.collectOneOffEvents -import com.instructure.pandautils.utils.expand -import com.instructure.pandautils.utils.hide -import com.instructure.pandautils.utils.show import com.instructure.parentapp.R import com.instructure.parentapp.databinding.ActivityMainBinding -import com.instructure.parentapp.databinding.NavigationDrawerHeaderLayoutBinding -import com.instructure.parentapp.util.ParentLogoutTask +import com.instructure.parentapp.features.splash.SplashFragment +import com.instructure.parentapp.util.navigation.Navigation import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch +import javax.inject.Inject + @AndroidEntryPoint class MainActivity : AppCompatActivity() { private val binding by viewBinding(ActivityMainBinding::inflate) - private val viewModel by viewModels() + @Inject + lateinit var navigation: Navigation private lateinit var navController: NavController - private lateinit var appBarConfiguration: AppBarConfiguration - private lateinit var headerLayoutBinding: NavigationDrawerHeaderLayoutBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding.lifecycleOwner = this - binding.viewModel = viewModel setContentView(binding.root) - setupNavigation() - handleDeeplink() - - lifecycleScope.collectOneOffEvents(viewModel.events, ::handleAction) - - lifecycleScope.launch { - viewModel.data.collectLatest { - setupNavigationDrawerHeader(it.userViewData) - setupAppColors(it.selectedStudent) - } - } } - override fun onSupportNavigateUp(): Boolean { - return navController.navigateUp(appBarConfiguration) - } - - private fun handleAction(action: MainAction) = when (action) { - is MainAction.ShowToast -> Toast.makeText(this, action.message, Toast.LENGTH_LONG).show() - is MainAction.LocaleChanged -> LocaleUtils.restartApp(this) + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + handleDeeplink(intent?.data) } private fun setupNavigation() { + val deeplinkUri = intent.data + intent.data = null + val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment navController = navHostFragment.navController + navController.graph = navigation.crateMainNavGraph(navController) - val drawerLayout = binding.drawerLayout - appBarConfiguration = AppBarConfiguration(setOf(R.id.courses, R.id.calendar, R.id.alerts), drawerLayout) - - val toolbar = binding.toolbar - toolbar.setNavigationIcon(R.drawable.ic_hamburger) - toolbar.navigationContentDescription = getString(R.string.navigation_drawer_open) - toolbar.setNavigationOnClickListener { - openNavigationDrawer() - } - - val navView = binding.navView - navView.setNavigationItemSelectedListener { - closeNavigationDrawer() - when (it.itemId) { - R.id.log_out -> { - onLogout() - true - } - - R.id.switch_users -> { - onSwitchUsers() - true - } - - else -> { - navController.navigate(it.itemId) - true - } - } - } - headerLayoutBinding = NavigationDrawerHeaderLayoutBinding.bind(navView.getHeaderView(0)) - - val bottomNavigationView = binding.bottomNav - bottomNavigationView.setupWithNavController(navController) - - // Hide bottom nav on screens which don't require it - navController.addOnDestinationChangedListener { _, destination, _ -> - when (destination.id) { - R.id.coursesFragment, R.id.calendarFragment, R.id.alertsFragment, R.id.help -> showMainNavigation() - else -> hideMainNavigation() - } - } - } - - private fun showMainNavigation() { - binding.bottomNav.show() - binding.toolbar.expand(200L, 100L, true) - binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) - } - - private fun hideMainNavigation() { - binding.bottomNav.hide() - binding.toolbar.collapse(200L, 100L, true) - viewModel.closeStudentSelector() - binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) - } + navController.currentBackStackEntry?.savedStateHandle?.getLiveData(SplashFragment.INITIAL_DATA_LOADED_KEY)?.observe(this) { + // If the initial data has been loaded, we can navigate to courses, remove splash from backstack + navController.popBackStack() + navController.navigate(navigation.courses) + navController.graph.setStartDestination(navigation.courses) - private fun setupAppColors(student: User?) { - val color = ColorKeeper.getOrGenerateUserColor(student).backgroundColor() - if (binding.toolbar.background == null) { - binding.toolbar.setBackgroundColor(color) - } else { - binding.toolbar.animateCircularBackgroundColorChange(color, binding.toolbarImage) + handleDeeplink(deeplinkUri) } - binding.bottomNav.applyTheme(color, getColor(R.color.textDarkest)) - ViewStyler.setStatusBarDark(this, color) } - private fun openNavigationDrawer() { - binding.drawerLayout.openDrawer(GravityCompat.START) - } - - private fun closeNavigationDrawer() { - binding.drawerLayout.closeDrawer(GravityCompat.START) - } - - private fun handleDeeplink() { + private fun handleDeeplink(uri: Uri?) { try { - navController.handleDeepLink(intent) + navController.navigate(uri ?: return) } catch (e: Exception) { - finish() + Log.e(this.javaClass.simpleName, e.message.orEmpty()) } } - private fun onLogout() { - AlertDialog.Builder(this) - .setTitle(R.string.logout_warning) - .setPositiveButton(android.R.string.ok) { _, _ -> - ParentLogoutTask(LogoutTask.Type.LOGOUT).execute() - } - .setNegativeButton(android.R.string.cancel, null) - .create() - .show() - } - - private fun onSwitchUsers() { - ParentLogoutTask(LogoutTask.Type.SWITCH_USERS).execute() - } - - private fun setupNavigationDrawerHeader(userViewData: UserViewData?) { - headerLayoutBinding.userViewData = userViewData - } - override fun onBackPressed() { - if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) { - closeNavigationDrawer() - return - } - // supportFragmentManager.fragments.last() is always the NavHostFragment val topFragment = supportFragmentManager.fragments.last().childFragmentManager.fragments.last() if (topFragment is NavigationCallbacks && topFragment.onHandleBackPressed()) { diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentsFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentsFragment.kt new file mode 100644 index 0000000000..08a5d109cd --- /dev/null +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentsFragment.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.parentapp.features.managestudents + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.material.Text +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment +import dagger.hilt.android.AndroidEntryPoint + + +@AndroidEntryPoint +class ManageStudentsFragment : Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return ComposeView(requireActivity()).apply { + setContent { + Text(text = "Manage Students") + } + } + } +} diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/notaparent/NotAParentFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/notaparent/NotAParentFragment.kt new file mode 100644 index 0000000000..e616c8f91c --- /dev/null +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/notaparent/NotAParentFragment.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.parentapp.features.notaparent + +import android.content.ActivityNotFoundException +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment +import com.instructure.loginapi.login.tasks.LogoutTask +import com.instructure.parentapp.util.ParentLogoutTask +import dagger.hilt.android.AndroidEntryPoint + + +@AndroidEntryPoint +class NotAParentFragment : Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return ComposeView(requireActivity()).apply { + setContent { + NotAParentScreen( + returnToLoginClick = { + ParentLogoutTask(LogoutTask.Type.LOGOUT).execute() + }, + onStudentClick = { + openStore("com.instructure.candroid") + }, + onTeacherClick = { + openStore("com.instructure.teacher") + } + ) + } + } + } + + private fun openStore(packageName: String) { + try { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=$packageName"))) + } catch (e: ActivityNotFoundException) { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=$packageName"))) + } + } +} diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/notaparent/NotAParentScreen.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/notaparent/NotAParentScreen.kt new file mode 100644 index 0000000000..01f7ce1118 --- /dev/null +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/notaparent/NotAParentScreen.kt @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.parentapp.features.notaparent + +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.instructure.pandautils.compose.composables.EmptyContent +import com.instructure.parentapp.R + + +@Composable +internal fun NotAParentScreen( + returnToLoginClick: () -> Unit, + onStudentClick: () -> Unit, + onTeacherClick: () -> Unit, + modifier: Modifier = Modifier +) { + Surface( + color = colorResource(id = R.color.backgroundLightest), + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + var bottomExpanded by remember { mutableStateOf(false) } + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(horizontal = 16.dp) + ) { + Spacer(modifier = Modifier.weight(1f)) + EmptyContent( + emptyTitle = stringResource(id = R.string.notAParentTitle), + emptyMessage = stringResource(id = R.string.notAParentSubtitle), + imageRes = R.drawable.ic_panda_book, + buttonText = stringResource(id = R.string.returnToLogin), + buttonClick = returnToLoginClick + ) + Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(16.dp)) + if (bottomExpanded) { + Spacer(modifier = Modifier.height(16.dp)) + Divider( + color = colorResource(id = R.color.backgroundMedium), + thickness = 1.dp + ) + Spacer(modifier = Modifier.height(16.dp)) + } + Text( + text = stringResource(id = R.string.studentOrTeacherTitle), + color = colorResource(id = R.color.textDarkest), + fontSize = 12.sp, + modifier = Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { + bottomExpanded = !bottomExpanded + } + ) + Spacer(modifier = Modifier.height(16.dp)) + AppOptions(bottomExpanded, onStudentClick, onTeacherClick) + } + } +} + +@Composable +private fun AppOptions( + bottomExpanded: Boolean, + onStudentClick: () -> Unit, + onTeacherClick: () -> Unit, + modifier: Modifier = Modifier +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .animateContentSize() + .height(if (bottomExpanded) 180.dp else 0.dp) + ) { + Text( + text = stringResource(id = R.string.studentOrTeacherSubtitle), + color = colorResource(id = R.color.textDarkest), + fontSize = 12.sp, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(16.dp)) + AppOption( + stringResource(id = R.string.studentApp), + stringResource(id = R.string.canvasStudentApp), + colorResource(id = R.color.login_studentAppTheme), + { onStudentClick() } + ) + Spacer(modifier = Modifier.height(12.dp)) + AppOption( + stringResource(id = R.string.teacherApp), + stringResource(id = R.string.canvasTeacherApp), + colorResource(id = R.color.login_teacherAppTheme), + { onTeacherClick() } + ) + } +} + +@Composable +private fun AppOption( + name: String, + label: String, + color: Color, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .clickable(onClick = onClick) + .semantics(mergeDescendants = true) { + contentDescription = label + } + ) { + Icon( + painter = painterResource(id = R.drawable.ic_canvas_logo), + tint = color, + contentDescription = null, + modifier = Modifier.size(48.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Column { + Icon( + painter = painterResource(id = R.drawable.ic_canvas_wordmark), + tint = colorResource(id = R.color.backgroundMedium), + contentDescription = null, + modifier = Modifier.height(24.dp) + ) + Text( + text = name, + color = color, + fontSize = 10.sp, + textAlign = TextAlign.Center + ) + } + } +} + +@Preview +@Composable +fun NotAParentScreenPreview() { + NotAParentScreen( + returnToLoginClick = {}, + onStudentClick = {}, + onTeacherClick = {} + ) +} \ No newline at end of file diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/settings/SettingsFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/settings/SettingsFragment.kt new file mode 100644 index 0000000000..3478ca10c1 --- /dev/null +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/settings/SettingsFragment.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.parentapp.features.settings + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.material.Text +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment + + +class SettingsFragment : Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return ComposeView(requireActivity()).apply { + setContent { + Text(text = "Settings") + } + } + } +} diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashAction.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashAction.kt new file mode 100644 index 0000000000..fe76d7d5e5 --- /dev/null +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashAction.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.parentapp.features.splash + + +sealed class SplashAction { + data object LocaleChanged : SplashAction() + data object InitialDataLoadingFinished : SplashAction() + data object NavigateToNotAParentScreen : SplashAction() +} diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashFragment.kt new file mode 100644 index 0000000000..3d3946067b --- /dev/null +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashFragment.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.parentapp.features.splash + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import com.instructure.canvasapi2.utils.LocaleUtils +import com.instructure.loginapi.login.view.CanvasLoadingView +import com.instructure.pandautils.utils.collectOneOffEvents +import com.instructure.parentapp.R +import com.instructure.parentapp.util.navigation.Navigation +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + + +@AndroidEntryPoint +class SplashFragment : Fragment() { + + private val viewModel: SplashViewModel by viewModels() + + @Inject + lateinit var navigation: Navigation + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + lifecycleScope.collectOneOffEvents(viewModel.events, ::handleAction) + + return ComposeView(requireActivity()).apply { + setContent { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxSize() + .background(color = colorResource(id = R.color.backgroundLightest)) + ) { + AndroidView( + factory = { + CanvasLoadingView(it).apply { + setOverrideColor(it.getColor(R.color.login_parentAppTheme)) + } + }, + modifier = Modifier.size(120.dp) + ) + } + } + } + } + + private fun handleAction(action: SplashAction) { + when (action) { + is SplashAction.LocaleChanged -> LocaleUtils.restartApp(requireContext()) + + is SplashAction.InitialDataLoadingFinished -> { + findNavController().currentBackStackEntry?.savedStateHandle?.set(INITIAL_DATA_LOADED_KEY, true) + } + + is SplashAction.NavigateToNotAParentScreen -> { + findNavController().popBackStack() + navigation.navigate(activity, navigation.notAParent) + } + } + } + + companion object { + const val INITIAL_DATA_LOADED_KEY = "initialDataLoaded" + } +} diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/main/MainRepository.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashRepository.kt similarity index 88% rename from apps/parent/src/main/java/com/instructure/parentapp/features/main/MainRepository.kt rename to apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashRepository.kt index 45916470d2..50318bb742 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/main/MainRepository.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashRepository.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.features.main +package com.instructure.parentapp.features.splash import com.instructure.canvasapi2.apis.EnrollmentAPI import com.instructure.canvasapi2.apis.ThemeAPI @@ -27,23 +27,12 @@ import com.instructure.canvasapi2.models.User import com.instructure.canvasapi2.utils.depaginate -class MainRepository( - private val enrollmentApi: EnrollmentAPI.EnrollmentInterface, +class SplashRepository( private val userApi: UserAPI.UsersInterface, - private val themeApi: ThemeAPI.ThemeInterface + private val themeApi: ThemeAPI.ThemeInterface, + private val enrollmentApi: EnrollmentAPI.EnrollmentInterface ) { - suspend fun getStudents(): List { - val params = RestParams(usePerPageQueryParam = true) - return enrollmentApi.firstPageObserveeEnrollmentsParent(params).depaginate { - enrollmentApi.getNextPage(it, params) - }.dataOrNull - .orEmpty() - .mapNotNull { it.observedUser } - .distinct() - .sortedBy { it.sortableName } - } - suspend fun getSelf(): User? { val params = RestParams(isForceReadFromNetwork = true) return userApi.getSelf(params).dataOrNull @@ -58,4 +47,13 @@ class MainRepository( val params = RestParams(isForceReadFromNetwork = true) return themeApi.getTheme(params).dataOrNull } + + suspend fun getStudents(): List { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = true) + return enrollmentApi.firstPageObserveeEnrollmentsParent(params).depaginate { + enrollmentApi.getNextPage(it, params) + }.dataOrNull + .orEmpty() + .mapNotNull { it.observedUser } + } } \ No newline at end of file diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashViewModel.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashViewModel.kt new file mode 100644 index 0000000000..dbb8f658f4 --- /dev/null +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/splash/SplashViewModel.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.parentapp.features.splash + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.weave.catch +import com.instructure.canvasapi2.utils.weave.tryLaunch +import com.instructure.pandautils.utils.ColorKeeper +import com.instructure.pandautils.utils.ThemePrefs +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + + +@HiltViewModel +class SplashViewModel @Inject constructor( + @ApplicationContext private val context: Context, + private val repository: SplashRepository, + private val apiPrefs: ApiPrefs, + private val colorKeeper: ColorKeeper, + private val themePrefs: ThemePrefs, +) : ViewModel() { + + private val _events = Channel() + val events = _events.receiveAsFlow() + + init { + loadInitialData() + } + + private fun loadInitialData() { + viewModelScope.tryLaunch { + val user = repository.getSelf() + user?.let { saveUserInfo(it) } + + val colors = repository.getColors() + colors?.let { colorKeeper.addToCache(it) } + + val theme = repository.getTheme() + theme?.let { themePrefs.applyCanvasTheme(it, context) } + + val students = repository.getStudents() + if (students.isEmpty()) { + _events.send(SplashAction.NavigateToNotAParentScreen) + } else { + _events.send(SplashAction.InitialDataLoadingFinished) + } + } catch { + viewModelScope.launch { + _events.send(SplashAction.InitialDataLoadingFinished) + } + } + } + + private suspend fun saveUserInfo(user: User) { + val oldLocale = apiPrefs.effectiveLocale + apiPrefs.user = user + if (apiPrefs.effectiveLocale != oldLocale) { + _events.send(SplashAction.LocaleChanged) + } + } +} diff --git a/apps/parent/src/main/java/com/instructure/parentapp/util/navigation/Navigation.kt b/apps/parent/src/main/java/com/instructure/parentapp/util/navigation/Navigation.kt new file mode 100644 index 0000000000..4e51d14a65 --- /dev/null +++ b/apps/parent/src/main/java/com/instructure/parentapp/util/navigation/Navigation.kt @@ -0,0 +1,117 @@ +package com.instructure.parentapp.util.navigation + +import android.app.Activity +import android.util.Log +import androidx.navigation.NavController +import androidx.navigation.NavGraph +import androidx.navigation.NavType +import androidx.navigation.createGraph +import androidx.navigation.findNavController +import androidx.navigation.fragment.dialog +import androidx.navigation.fragment.fragment +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.pandautils.features.help.HelpDialogFragment +import com.instructure.pandautils.features.inbox.list.InboxFragment +import com.instructure.parentapp.R +import com.instructure.parentapp.features.alerts.list.AlertsFragment +import com.instructure.parentapp.features.calendar.CalendarFragment +import com.instructure.parentapp.features.courses.details.CourseDetailsFragment +import com.instructure.parentapp.features.courses.list.CoursesFragment +import com.instructure.parentapp.features.dashboard.DashboardFragment +import com.instructure.parentapp.features.managestudents.ManageStudentsFragment +import com.instructure.parentapp.features.notaparent.NotAParentFragment +import com.instructure.parentapp.features.settings.SettingsFragment +import com.instructure.parentapp.features.splash.SplashFragment + + +class Navigation(apiPrefs: ApiPrefs) { + + private val baseUrl = apiPrefs.fullDomain + + private val courseId = "course-id" + private val courseDetails = "$baseUrl/courses/{$courseId}" + + val splash = "$baseUrl/splash" + val notAParent = "$baseUrl/not-a-parent" + val courses = "$baseUrl/courses" + val calendar = "$baseUrl/calendar" + val alerts = "$baseUrl/alerts" + val inbox = "$baseUrl/conversations" + val help = "$baseUrl/help" + val manageStudents = "$baseUrl/manage-students" + val settings = "$baseUrl/settings" + + fun courseDetailsRoute(id: Long) = "$baseUrl/courses/$id" + + fun crateMainNavGraph(navController: NavController): NavGraph { + return navController.createGraph( + splash + ) { + fragment(splash) + fragment(notAParent) + fragment(courses) { + deepLink { + uriPattern = courses + } + } + fragment(calendar) { + deepLink { + uriPattern = calendar + } + } + fragment(alerts) { + deepLink { + uriPattern = alerts + } + } + fragment(inbox) { + deepLink { + uriPattern = inbox + } + } + fragment(manageStudents) + fragment(settings) + dialog(help) + fragment(courseDetails) { + argument(courseId) { + type = NavType.LongType + nullable = false + } + deepLink { + uriPattern = courseDetails + } + } + } + } + + fun createDashboardNavGraph(navController: NavController): NavGraph { + return navController.createGraph( + courses + ) { + fragment(courses) { + deepLink { + uriPattern = courses + } + } + fragment(calendar) { + deepLink { + uriPattern = calendar + } + } + fragment(alerts) { + deepLink { + uriPattern = alerts + } + } + } + } + + fun navigate(activity: Activity?, route: String) { + val navController = activity?.findNavController(R.id.nav_host_fragment) ?: return + try { + navController.navigate(route) + } catch (e: Exception) { + Log.e(this.javaClass.simpleName, e.message.orEmpty()) + } + } +} diff --git a/apps/parent/src/main/res/layout/activity_main.xml b/apps/parent/src/main/res/layout/activity_main.xml index 5ead5ccf49..8877a64b49 100644 --- a/apps/parent/src/main/res/layout/activity_main.xml +++ b/apps/parent/src/main/res/layout/activity_main.xml @@ -13,164 +13,10 @@ ~ You should have received a copy of the GNU General Public License ~ along with this program. If not, see . --> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:id="@+id/nav_host_fragment" + android:name="androidx.navigation.fragment.NavHostFragment" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:defaultNavHost="true" /> diff --git a/apps/parent/src/main/res/layout/fragment_alerts.xml b/apps/parent/src/main/res/layout/fragment_alerts.xml deleted file mode 100644 index aa9e3988ec..0000000000 --- a/apps/parent/src/main/res/layout/fragment_alerts.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - \ No newline at end of file diff --git a/apps/parent/src/main/res/layout/fragment_calendar.xml b/apps/parent/src/main/res/layout/fragment_calendar.xml deleted file mode 100644 index 29581224c9..0000000000 --- a/apps/parent/src/main/res/layout/fragment_calendar.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - \ No newline at end of file diff --git a/apps/parent/src/main/res/layout/fragment_course_details.xml b/apps/parent/src/main/res/layout/fragment_course_details.xml deleted file mode 100644 index ecf726f84c..0000000000 --- a/apps/parent/src/main/res/layout/fragment_course_details.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - \ No newline at end of file diff --git a/apps/parent/src/main/res/layout/fragment_dashboard.xml b/apps/parent/src/main/res/layout/fragment_dashboard.xml new file mode 100644 index 0000000000..3e21be83f1 --- /dev/null +++ b/apps/parent/src/main/res/layout/fragment_dashboard.xml @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/parent/src/main/res/layout/item_student.xml b/apps/parent/src/main/res/layout/item_student.xml index 179194f376..526266e407 100644 --- a/apps/parent/src/main/res/layout/item_student.xml +++ b/apps/parent/src/main/res/layout/item_student.xml @@ -20,7 +20,7 @@ + type="com.instructure.parentapp.features.dashboard.StudentItemViewModel" /> + type="com.instructure.parentapp.features.dashboard.UserViewData" /> diff --git a/apps/parent/src/main/res/menu/bottom_nav.xml b/apps/parent/src/main/res/menu/bottom_nav.xml index 17f7c53e08..8e9767c99b 100644 --- a/apps/parent/src/main/res/menu/bottom_nav.xml +++ b/apps/parent/src/main/res/menu/bottom_nav.xml @@ -17,16 +17,16 @@ diff --git a/apps/parent/src/main/res/navigation/alerts.xml b/apps/parent/src/main/res/navigation/alerts.xml deleted file mode 100644 index 34c88da0ed..0000000000 --- a/apps/parent/src/main/res/navigation/alerts.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - diff --git a/apps/parent/src/main/res/navigation/calendar.xml b/apps/parent/src/main/res/navigation/calendar.xml deleted file mode 100644 index a9ad012802..0000000000 --- a/apps/parent/src/main/res/navigation/calendar.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - diff --git a/apps/parent/src/main/res/navigation/courses.xml b/apps/parent/src/main/res/navigation/courses.xml deleted file mode 100644 index 6e1397fc43..0000000000 --- a/apps/parent/src/main/res/navigation/courses.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - diff --git a/apps/parent/src/main/res/navigation/nav_graph.xml b/apps/parent/src/main/res/navigation/nav_graph.xml deleted file mode 100644 index 08de9a6b10..0000000000 --- a/apps/parent/src/main/res/navigation/nav_graph.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/courses/list/CoursesViewModelTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/courses/list/CoursesViewModelTest.kt index ceabd4f378..557a2837b1 100644 --- a/apps/parent/src/test/java/com/instructure/parentapp/features/courses/list/CoursesViewModelTest.kt +++ b/apps/parent/src/test/java/com/instructure/parentapp/features/courses/list/CoursesViewModelTest.kt @@ -23,10 +23,9 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.User -import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.pandautils.utils.ColorKeeper import com.instructure.pandautils.utils.ThemedColor -import com.instructure.parentapp.features.main.TestSelectStudentHolder +import com.instructure.parentapp.features.dashboard.TestSelectStudentHolder import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every @@ -59,7 +58,6 @@ class CoursesViewModelTest { private val repository: CoursesRepository = mockk(relaxed = true) private val colorKeeper: ColorKeeper = mockk(relaxed = true) - private val apiPrefs: ApiPrefs = mockk(relaxed = true) private val selectedStudentFlow = MutableSharedFlow() private val selectedStudentHolder = TestSelectStudentHolder(selectedStudentFlow) private val courseGradeFormatter: CourseGradeFormatter = mockk(relaxed = true) @@ -164,8 +162,6 @@ class CoursesViewModelTest { @Test fun `Navigate to course details`() = runTest { - coEvery { apiPrefs.fullDomain } returns "https://canvas.instructure.com" - createViewModel() val events = mutableListOf() @@ -175,11 +171,11 @@ class CoursesViewModelTest { viewModel.handleAction(CoursesAction.CourseTapped(1L)) - val expected = CoursesViewModelAction.NavigateToCourseDetails("https://canvas.instructure.com/courses/1") + val expected = CoursesViewModelAction.NavigateToCourseDetails(1L) Assert.assertEquals(expected, events.last()) } private fun createViewModel() { - viewModel = CoursesViewModel(repository, colorKeeper, apiPrefs, selectedStudentHolder, courseGradeFormatter) + viewModel = CoursesViewModel(repository, colorKeeper, selectedStudentHolder, courseGradeFormatter) } } diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/DashboardRepositoryTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/DashboardRepositoryTest.kt new file mode 100644 index 0000000000..90af5de3e1 --- /dev/null +++ b/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/DashboardRepositoryTest.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.parentapp.features.dashboard + +import com.instructure.canvasapi2.apis.EnrollmentAPI +import com.instructure.canvasapi2.models.Enrollment +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.LinkHeaders +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Test + + +class DashboardRepositoryTest { + + private val enrollmentApi: EnrollmentAPI.EnrollmentInterface = mockk(relaxed = true) + + private val repository = DashboardRepository(enrollmentApi) + + @Test + fun `Get students successfully returns data`() = runTest { + val expected = listOf(User(id = 1L)) + val enrollments = expected.map { Enrollment(observedUser = it) } + + coEvery { enrollmentApi.firstPageObserveeEnrollmentsParent(any()) } returns DataResult.Success(enrollments) + + val result = repository.getStudents() + Assert.assertEquals(expected, result) + } + + @Test + fun `Get students with pagination successfully returns data`() = runTest { + val page1 = listOf(User(id = 1L)) + val enrollments1 = page1.map { Enrollment(observedUser = it) } + val page2 = listOf(User(id = 2L)) + val enrollments2 = page2.map { Enrollment(observedUser = it) } + + coEvery { enrollmentApi.firstPageObserveeEnrollmentsParent(any()) } returns DataResult.Success( + enrollments1, + linkHeaders = LinkHeaders(nextUrl = "page_2_url") + ) + coEvery { enrollmentApi.getNextPage("page_2_url", any()) } returns DataResult.Success(enrollments2) + + val result = repository.getStudents() + Assert.assertEquals(page1 + page2, result) + } + + @Test + fun `Get students returns data distinctly and sorted`() = runTest { + val expected = listOf(User(id = 1L, sortableName = "First"), User(id = 2L, sortableName = "Second")) + val enrollments = expected.asReversed().map { Enrollment(observedUser = it) } + val otherEnrollments = listOf( + Enrollment(user = User(id = 3L)), + Enrollment(observedUser = User(id = 1L)) + ) + + coEvery { enrollmentApi.firstPageObserveeEnrollmentsParent(any()) } returns DataResult.Success(enrollments + otherEnrollments) + + val result = repository.getStudents() + Assert.assertEquals(expected, result) + } +} diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/main/MainViewModelTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/DashboardViewModelTest.kt similarity index 72% rename from apps/parent/src/test/java/com/instructure/parentapp/features/main/MainViewModelTest.kt rename to apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/DashboardViewModelTest.kt index 70f6bc71c1..024e1d6bc2 100644 --- a/apps/parent/src/test/java/com/instructure/parentapp/features/main/MainViewModelTest.kt +++ b/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/DashboardViewModelTest.kt @@ -15,36 +15,28 @@ * */ -package com.instructure.parentapp.features.main +package com.instructure.parentapp.features.dashboard import android.content.Context import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry -import com.instructure.canvasapi2.models.CanvasColor -import com.instructure.canvasapi2.models.CanvasTheme import com.instructure.canvasapi2.models.User import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.ContextKeeper import com.instructure.loginapi.login.model.SignedInUser import com.instructure.loginapi.login.util.PreviousUsersUtils import com.instructure.pandautils.mvvm.ViewState -import com.instructure.pandautils.utils.ColorKeeper -import com.instructure.pandautils.utils.ThemePrefs import com.instructure.parentapp.R import com.instructure.parentapp.util.ParentPrefs import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Assert @@ -54,7 +46,7 @@ import org.junit.Test @ExperimentalCoroutinesApi -class MainViewModelTest { +class DashboardViewModelTest { @get:Rule val instantExecutorRule = InstantTaskExecutorRule() @@ -64,15 +56,13 @@ class MainViewModelTest { private val testDispatcher = UnconfinedTestDispatcher() private val context: Context = mockk(relaxed = true) - private val repository: MainRepository = mockk(relaxed = true) + private val repository: DashboardRepository = mockk(relaxed = true) private val previousUsersUtils: PreviousUsersUtils = mockk(relaxed = true) private val apiPrefs: ApiPrefs = mockk(relaxed = true) private val parentPrefs: ParentPrefs = mockk(relaxed = true) - private val colorKeeper: ColorKeeper = mockk(relaxed = true) - private val themePrefs: ThemePrefs = mockk(relaxed = true) private val selectedStudentHolder: SelectedStudentHolder = mockk(relaxed = true) - private lateinit var viewModel: MainViewModel + private lateinit var viewModel: DashboardViewModel @Before fun setup() { @@ -86,46 +76,6 @@ class MainViewModelTest { Dispatchers.resetMain() } - @Test - fun `Load and store initial data`() { - val user = User(id = 1L) - coEvery { repository.getSelf() } returns user - - val colors = CanvasColor() - coEvery { repository.getColors() } returns colors - - val theme = CanvasTheme("", "", "", "", "", "", "", "") - coEvery { repository.getTheme() } returns theme - - val students = User(id = 2L) - coEvery { repository.getStudents() } returns listOf(students) - - createViewModel() - - coVerify { apiPrefs.user = user } - coVerify { colorKeeper.addToCache(colors) } - coVerify { themePrefs.applyCanvasTheme(theme, context) } - Assert.assertEquals(ViewState.Success, viewModel.state.value) - } - - @Test - fun `User stored and locale changed`() = runTest { - val user = User(id = 1L, effective_locale = "en") - coEvery { repository.getSelf() } returns user - every { apiPrefs.user = any() } answers { - every { apiPrefs.effectiveLocale } returns user.effective_locale.orEmpty() - } - - createViewModel() - - val events = mutableListOf() - backgroundScope.launch(testDispatcher) { - viewModel.events.toList(events) - } - - Assert.assertEquals(MainAction.LocaleChanged, events.last()) - } - @Test fun `Load user info correctly`() { val user = User( @@ -235,26 +185,13 @@ class MainViewModelTest { Assert.assertFalse(viewModel.data.value.studentSelectorExpanded) } - @Test - fun `Close student selector`() { - createViewModel() - - viewModel.toggleStudentSelector() - Assert.assertTrue(viewModel.data.value.studentSelectorExpanded) - - viewModel.closeStudentSelector() - Assert.assertFalse(viewModel.data.value.studentSelectorExpanded) - } - private fun createViewModel() { - viewModel = MainViewModel( + viewModel = DashboardViewModel( context = context, repository = repository, previousUsersUtils = previousUsersUtils, apiPrefs = apiPrefs, parentPrefs = parentPrefs, - colorKeeper = colorKeeper, - themePrefs = themePrefs, selectedStudentHolder = selectedStudentHolder ) } diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/main/TestSelectStudentHolder.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/TestSelectStudentHolder.kt similarity index 94% rename from apps/parent/src/test/java/com/instructure/parentapp/features/main/TestSelectStudentHolder.kt rename to apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/TestSelectStudentHolder.kt index c8c2aef691..6c34d1556e 100644 --- a/apps/parent/src/test/java/com/instructure/parentapp/features/main/TestSelectStudentHolder.kt +++ b/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/TestSelectStudentHolder.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.features.main +package com.instructure.parentapp.features.dashboard import com.instructure.canvasapi2.models.User import kotlinx.coroutines.flow.MutableSharedFlow diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/main/MainRepositoryTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/splash/SplashRepositoryTest.kt similarity index 84% rename from apps/parent/src/test/java/com/instructure/parentapp/features/main/MainRepositoryTest.kt rename to apps/parent/src/test/java/com/instructure/parentapp/features/splash/SplashRepositoryTest.kt index 1a97fbc762..f27d515db4 100644 --- a/apps/parent/src/test/java/com/instructure/parentapp/features/main/MainRepositoryTest.kt +++ b/apps/parent/src/test/java/com/instructure/parentapp/features/splash/SplashRepositoryTest.kt @@ -15,7 +15,7 @@ * */ -package com.instructure.parentapp.features.main +package com.instructure.parentapp.features.splash import com.instructure.canvasapi2.apis.EnrollmentAPI import com.instructure.canvasapi2.apis.ThemeAPI @@ -33,14 +33,13 @@ import org.junit.Assert import org.junit.Test -class MainRepositoryTest { +class SplashRepositoryTest { - private val enrollmentApi: EnrollmentAPI.EnrollmentInterface = mockk(relaxed = true) private val themeApi: ThemeAPI.ThemeInterface = mockk(relaxed = true) private val userApi: UserAPI.UsersInterface = mockk(relaxed = true) + private val enrollmentApi: EnrollmentAPI.EnrollmentInterface = mockk(relaxed = true) - - private val repository = MainRepository(enrollmentApi, userApi, themeApi) + private val repository = SplashRepository(userApi, themeApi, enrollmentApi) @Test fun `Get students successfully returns data`() = runTest { @@ -70,21 +69,6 @@ class MainRepositoryTest { Assert.assertEquals(page1 + page2, result) } - @Test - fun `Get students returns data distinctly and sorted`() = runTest { - val expected = listOf(User(id = 1L, sortableName = "First"), User(id = 2L, sortableName = "Second")) - val enrollments = expected.asReversed().map { Enrollment(observedUser = it) } - val otherEnrollments = listOf( - Enrollment(user = User(id = 3L)), - Enrollment(observedUser = User(id = 1L)) - ) - - coEvery { enrollmentApi.firstPageObserveeEnrollmentsParent(any()) } returns DataResult.Success(enrollments + otherEnrollments) - - val result = repository.getStudents() - Assert.assertEquals(expected, result) - } - @Test fun `Get students returns empty list when enrollments call fails`() = runTest { coEvery { enrollmentApi.firstPageObserveeEnrollmentsParent(any()) } returns DataResult.Fail() diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/splash/SplashViewModelTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/splash/SplashViewModelTest.kt new file mode 100644 index 0000000000..69695cb976 --- /dev/null +++ b/apps/parent/src/test/java/com/instructure/parentapp/features/splash/SplashViewModelTest.kt @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.parentapp.features.splash + +import android.content.Context +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import com.instructure.canvasapi2.models.CanvasColor +import com.instructure.canvasapi2.models.CanvasTheme +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.ContextKeeper +import com.instructure.pandautils.utils.ColorKeeper +import com.instructure.pandautils.utils.ThemePrefs +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Rule +import org.junit.Test + + +@ExperimentalCoroutinesApi +class SplashViewModelTest { + + @get:Rule + val instantExecutorRule = InstantTaskExecutorRule() + + private val lifecycleOwner: LifecycleOwner = mockk(relaxed = true) + private val lifecycleRegistry = LifecycleRegistry(lifecycleOwner) + private val testDispatcher = UnconfinedTestDispatcher() + + private val context: Context = mockk(relaxed = true) + private val repository: SplashRepository = mockk(relaxed = true) + private val apiPrefs: ApiPrefs = mockk(relaxed = true) + private val colorKeeper: ColorKeeper = mockk(relaxed = true) + private val themePrefs: ThemePrefs = mockk(relaxed = true) + + private lateinit var viewModel: SplashViewModel + + @Before + fun setup() { + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) + Dispatchers.setMain(testDispatcher) + ContextKeeper.appContext = context + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `Load and store initial data successfully`() = runTest { + val user = User(id = 1L) + coEvery { repository.getSelf() } returns user + + val colors = CanvasColor() + coEvery { repository.getColors() } returns colors + + val theme = CanvasTheme("", "", "", "", "", "", "", "") + coEvery { repository.getTheme() } returns theme + + val students = User(id = 2L) + coEvery { repository.getStudents() } returns listOf(students) + + createViewModel() + + coVerify { apiPrefs.user = user } + coVerify { colorKeeper.addToCache(colors) } + coVerify { themePrefs.applyCanvasTheme(theme, context) } + + val events = mutableListOf() + backgroundScope.launch(testDispatcher) { + viewModel.events.toList(events) + } + + Assert.assertEquals(SplashAction.InitialDataLoadingFinished, events.last()) + } + + @Test + fun `Load and store initial data fails`() = runTest { + coEvery { repository.getSelf() } throws Exception() + + createViewModel() + + val events = mutableListOf() + backgroundScope.launch(testDispatcher) { + viewModel.events.toList(events) + } + + Assert.assertEquals(SplashAction.InitialDataLoadingFinished, events.last()) + } + + @Test + fun `No observed students`() = runTest { + val user = User(id = 1L) + coEvery { repository.getSelf() } returns user + + val colors = CanvasColor() + coEvery { repository.getColors() } returns colors + + val theme = CanvasTheme("", "", "", "", "", "", "", "") + coEvery { repository.getTheme() } returns theme + + coEvery { repository.getStudents() } returns emptyList() + + createViewModel() + + val events = mutableListOf() + backgroundScope.launch(testDispatcher) { + viewModel.events.toList(events) + } + + Assert.assertEquals(SplashAction.NavigateToNotAParentScreen, events.last()) + } + + @Test + fun `User stored and locale changed`() = runTest { + val user = User(id = 1L, effective_locale = "en") + coEvery { repository.getSelf() } returns user + every { apiPrefs.user = any() } answers { + every { apiPrefs.effectiveLocale } returns user.effective_locale.orEmpty() + } + + createViewModel() + + val events = mutableListOf() + backgroundScope.launch(testDispatcher) { + viewModel.events.toList(events) + } + + Assert.assertEquals(SplashAction.LocaleChanged, events.first()) + } + + private fun createViewModel() { + viewModel = SplashViewModel( + context = context, + repository = repository, + apiPrefs = apiPrefs, + colorKeeper = colorKeeper, + themePrefs = themePrefs, + ) + } +} diff --git a/libs/pandares/src/main/res/values/strings.xml b/libs/pandares/src/main/res/values/strings.xml index 0be7305c7f..717dc3e750 100644 --- a/libs/pandares/src/main/res/values/strings.xml +++ b/libs/pandares/src/main/res/values/strings.xml @@ -1726,6 +1726,14 @@ End time cannot be before start time. Select all Deselect all + Title + Title + %s, selected calendar + %s selected + Open actions + Close actions + Actions open. Navigate forward for actions + Actions closed Alerts @@ -1739,16 +1747,17 @@ Log Out No Students You are not observing any students. - Title - Title No Grade There was an error loading your student’s courses. No Courses Your student’s courses might not be published yet. - %s, selected calendar - %s selected - Open actions - Close actions - Actions open. Navigate forward for actions - Actions closed + Not a parent? + We couldn\'t find any students associated with your account + Return to login + Are you a student or teacher? + One of our other apps might be a better fit. Tap one to visit the Play Store + STUDENT + Canvas Student + TEACHER + Canvas Teacher diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/EmptyContent.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/EmptyContent.kt index 9659cfa5a8..64c638e09f 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/EmptyContent.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/EmptyContent.kt @@ -16,6 +16,7 @@ package com.instructure.pandautils.compose.composables import androidx.annotation.DrawableRes +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -23,6 +24,9 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.OutlinedButton import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -41,7 +45,9 @@ fun EmptyContent( emptyTitle: String, emptyMessage: String, @DrawableRes imageRes: Int, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + buttonText: String? = null, + buttonClick: (() -> Unit)? = null ) { Column( verticalArrangement = Arrangement.Center, @@ -75,15 +81,36 @@ fun EmptyContent( textAlign = TextAlign.Center, modifier = Modifier.padding(horizontal = 32.dp) ) + buttonClick?.let { + Spacer(modifier = Modifier.height(32.dp)) + OutlinedButton( + onClick = { it() }, + border = BorderStroke(1.dp, colorResource(id = R.color.textDark)), + shape = RoundedCornerShape(4.dp), + colors = ButtonDefaults.buttonColors(backgroundColor = colorResource(id = R.color.backgroundLightest)) + ) { + Text( + text = buttonText.orEmpty(), + fontSize = 16.sp, + color = colorResource( + id = R.color.textDark + ), + textAlign = TextAlign.Center + ) + } + } } } + @Preview(showBackground = true) @Composable fun EmptyContentPreview() { EmptyContent( emptyTitle = "Empty Title", emptyMessage = "Empty Message", - imageRes = R.drawable.ic_panda_book + imageRes = R.drawable.ic_panda_book, + buttonText = "Button Text", + buttonClick = {} ) } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ViewExtensions.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ViewExtensions.kt index e322ecbe11..1fec3270e9 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ViewExtensions.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ViewExtensions.kt @@ -18,7 +18,6 @@ package com.instructure.pandautils.utils -import android.animation.ValueAnimator import android.annotation.SuppressLint import android.app.Activity import android.app.Dialog @@ -27,7 +26,6 @@ import android.content.res.ColorStateList import android.content.res.Resources import android.content.res.TypedArray import android.graphics.* -import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.net.Uri @@ -41,7 +39,6 @@ import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo import android.view.animation.AlphaAnimation import android.view.animation.Animation -import android.view.animation.AnimationUtils import android.view.animation.DecelerateInterpolator import android.view.animation.Transformation import android.view.inputmethod.InputMethodManager @@ -58,7 +55,6 @@ import androidx.core.animation.doOnStart import androidx.core.content.ContextCompat import androidx.core.graphics.ColorUtils import androidx.core.view.ViewCompat -import androidx.core.view.drawToBitmap import androidx.fragment.app.Fragment import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide @@ -71,7 +67,6 @@ import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.target.Target import com.bumptech.glide.signature.ObjectKey -import com.google.android.material.bottomnavigation.BottomNavigationView import com.instructure.canvasapi2.models.Attachment import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.utils.APIHelper @@ -827,91 +822,7 @@ fun Animation.addListener(onStart: (Animation?) -> Unit = {}, onEnd: (Animation? }) } -/** - * Potentially animate showing a [BottomNavigationView]. - * - * Abruptly changing the visibility leads to a re-layout of main content, animating - * `translationY` leaves a gap where the view was that content does not fill. - * - * Instead, take a snapshot of the view, and animate this in, only changing the visibility (and - * thus layout) when the animation completes. - */ -fun BottomNavigationView.show() { - if (visibility == View.VISIBLE) return - - this.post { - val parent = parent as ViewGroup - // View needs to be laid out to create a snapshot & know position to animate. If view isn't - // laid out yet, need to do this manually. - if (!isLaidOut) { - measure( - View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY), - View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.AT_MOST) - ) - layout(parent.left, parent.height - measuredHeight, parent.right, parent.height) - } - - val drawable = BitmapDrawable(context.resources, drawToBitmap()) - drawable.setBounds(left, parent.height, right, parent.height + height) - parent.overlay.add(drawable) - ValueAnimator.ofInt(parent.height, top).apply { - startDelay = 100L - duration = 300L - interpolator = AnimationUtils.loadInterpolator( - context, - android.R.interpolator.linear_out_slow_in - ) - addUpdateListener { - val newTop = it.animatedValue as Int - drawable.setBounds(left, newTop, right, newTop + height) - } - doOnEnd { - parent.overlay.remove(drawable) - visibility = View.VISIBLE - } - start() - } - } -} - -/** - * Potentially animate hiding a [BottomNavigationView]. - * - * Abruptly changing the visibility leads to a re-layout of main content, animating - * `translationY` leaves a gap where the view was that content does not fill. - * - * Instead, take a snapshot, instantly hide the view (so content lays out to fill), then animate - * out the snapshot. - */ -fun BottomNavigationView.hide() { - if (visibility == View.GONE) return - - this.post { - val drawable = BitmapDrawable(context.resources, drawToBitmap()) - val parent = parent as ViewGroup - drawable.setBounds(left, top, right, bottom) - parent.overlay.add(drawable) - visibility = View.GONE - ValueAnimator.ofInt(top, parent.height).apply { - startDelay = 100L - duration = 200L - interpolator = AnimationUtils.loadInterpolator( - context, - android.R.interpolator.fast_out_linear_in - ) - addUpdateListener { - val newTop = it.animatedValue as Int - drawable.setBounds(left, newTop, right, newTop + height) - } - doOnEnd { - parent.overlay.remove(drawable) - } - start() - } - } -} - -fun View.expand(duration: Long = 300, startOffset: Long = 0L, interpolate: Boolean = false) { +fun View.expand(duration: Long = 300) { if (visibility == View.VISIBLE) return this.measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) @@ -931,21 +842,12 @@ fun View.expand(duration: Long = 300, startOffset: Long = 0L, interpolate: Boole override fun willChangeBounds(): Boolean = true } - animation.apply { - this.startOffset = startOffset - this.duration = duration - if (interpolate) { - this.interpolator = AnimationUtils.loadInterpolator( - context, - android.R.interpolator.linear_out_slow_in - ) - } - } + animation.duration = duration this.startAnimation(animation) } -fun View.collapse(duration: Long = 300, startOffset: Long = 0L, interpolate: Boolean = false) { +fun View.collapse(duration: Long = 300) { if (visibility == View.GONE) return val initialHeight = this.measuredHeight @@ -963,16 +865,7 @@ fun View.collapse(duration: Long = 300, startOffset: Long = 0L, interpolate: Boo override fun willChangeBounds(): Boolean = true } - animation.apply { - this.startOffset = startOffset - this.duration = duration - if (interpolate) { - this.interpolator = AnimationUtils.loadInterpolator( - context, - android.R.interpolator.fast_out_linear_in - ) - } - } + animation.duration = duration this.startAnimation(animation) } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ViewStyler.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ViewStyler.kt index d102f15741..99afd1899e 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ViewStyler.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ViewStyler.kt @@ -36,7 +36,12 @@ import android.widget.ProgressBar import androidx.annotation.ColorInt import androidx.annotation.DimenRes import androidx.appcompat.app.AlertDialog -import androidx.appcompat.widget.* +import androidx.appcompat.widget.AppCompatCheckBox +import androidx.appcompat.widget.AppCompatEditText +import androidx.appcompat.widget.AppCompatRadioButton +import androidx.appcompat.widget.AppCompatSpinner +import androidx.appcompat.widget.SwitchCompat +import androidx.appcompat.widget.Toolbar import androidx.core.content.ContextCompat import androidx.core.view.ViewCompat import com.google.android.material.bottomnavigation.BottomNavigationView @@ -253,12 +258,12 @@ fun SwitchCompat.applyTheme(@ColorInt color: Int = ThemePrefs.brandColor) { ViewStyler.themeSwitch(context, this, color) } -fun AlertDialog.Builder.showThemed() { +fun AlertDialog.Builder.showThemed(@ColorInt color: Int = ThemePrefs.textButtonColor) { val dialog = create() dialog.setOnShowListener { - dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(ThemePrefs.textButtonColor) - dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(ThemePrefs.textButtonColor) - dialog.getButton(AlertDialog.BUTTON_NEUTRAL)?.setTextColor(ThemePrefs.textButtonColor) + dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(color) + dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(color) + dialog.getButton(AlertDialog.BUTTON_NEUTRAL)?.setTextColor(color) } dialog.show() } From decd45a57d6f4e1f70ad387d910a117cacfbfb2e Mon Sep 17 00:00:00 2001 From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com> Date: Thu, 11 Jul 2024 08:22:35 +0200 Subject: [PATCH 06/50] [MBL-17609][Parent] Inbox list integration (#2493) refs: MBL-17609 affects: Parent release note: none * Api calls and Toolbar shadow. * Unit tests. * Inbox unread count logic * Inbox count design. * Inbox count badge for drawer icon. * A11y * trigger PR build. * Unit tests. * Extracted Inbox integration tests to common code. * Parent interaction tests. --- apps/parent/build.gradle | 2 + .../ParentInboxListInteractionTest.kt | 106 +++++ .../parentapp/ui/pages/DashboardPage.kt | 8 +- .../parentapp/utils/ParentTestExtensions.kt | 7 +- .../parentapp/di/DashboardModule.kt | 14 +- .../features/dashboard/DashboardFragment.kt | 58 ++- .../features/dashboard/DashboardRepository.kt | 11 +- .../features/dashboard/DashboardViewData.kt | 3 +- .../features/dashboard/DashboardViewModel.kt | 23 +- .../features/dashboard/InboxCountUpdater.kt | 35 ++ .../inbox/list/ParentInboxRepository.kt | 3 +- .../parentapp/features/main/MainActivity.kt | 17 +- .../main/res/layout/fragment_dashboard.xml | 126 +++-- apps/parent/src/main/res/menu/nav_drawer.xml | 6 +- .../dashboard/DashboardRepositoryTest.kt | 36 +- .../dashboard/DashboardViewModelTest.kt | 42 +- .../dashboard/TestInboxCountUpdater.kt | 27 ++ .../inbox/list/ParentInboxRepositoryTest.kt | 63 +++ .../student/ui/e2e/InboxE2ETest.kt | 34 +- .../interaction/DashboardInteractionTest.kt | 2 +- .../ElementaryDashboardInteractionTest.kt | 2 +- ...kt => InboxConversationInteractionTest.kt} | 425 +---------------- .../StudentInboxListInteractionsTest.kt | 79 ++++ .../student/ui/pages/DashboardPage.kt | 5 + .../student/ui/utils/StudentTest.kt | 2 +- .../teacher/ui/AddMessagePageTest.kt | 4 +- .../teacher/ui/ChooseRecipientsPageTest.kt | 2 +- .../teacher/ui/InboxMessagePageTest.kt | 2 +- .../teacher/ui/TeacherInboxListPageTest.kt | 80 ++++ .../teacher/ui/e2e/InboxE2ETest.kt | 96 ++-- .../teacher/ui/e2e/PeopleE2ETest.kt | 2 +- .../instructure/teacher/ui/pages/InboxPage.kt | 443 ------------------ .../teacher/ui/utils/TeacherTest.kt | 2 +- .../interaction/InboxListInteractionTest.kt | 348 ++++++++------ .../espresso/common}/pages/InboxPage.kt | 58 +-- .../instructure/canvasapi2/apis/CourseAPI.kt | 3 + .../canvasapi2/apis/UnreadCountAPI.kt | 9 +- .../instructure/canvasapi2/di/ApiModule.kt | 5 + .../bg_button_full_rounded_filled.xml | 23 + ...button_full_rounded_filled_with_border.xml | 25 + libs/pandares/src/main/res/values/strings.xml | 2 + .../src/main/res/layout/fragment_inbox.xml | 3 +- 42 files changed, 1036 insertions(+), 1207 deletions(-) create mode 100644 apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxListInteractionTest.kt create mode 100644 apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/InboxCountUpdater.kt create mode 100644 apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/TestInboxCountUpdater.kt create mode 100644 apps/parent/src/test/java/com/instructure/parentapp/features/inbox/list/ParentInboxRepositoryTest.kt rename apps/student/src/androidTest/java/com/instructure/student/ui/interaction/{InboxInteractionTest.kt => InboxConversationInteractionTest.kt} (53%) create mode 100644 apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxListInteractionsTest.kt create mode 100644 apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherInboxListPageTest.kt delete mode 100644 apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/InboxPage.kt rename apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InboxPageTest.kt => automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxListInteractionTest.kt (59%) rename {apps/student/src/androidTest/java/com/instructure/student/ui => automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common}/pages/InboxPage.kt (89%) create mode 100644 libs/pandares/src/main/res/drawable/bg_button_full_rounded_filled.xml create mode 100644 libs/pandares/src/main/res/drawable/bg_button_full_rounded_filled_with_border.xml diff --git a/apps/parent/build.gradle b/apps/parent/build.gradle index 715191d39e..6f22fb2773 100644 --- a/apps/parent/build.gradle +++ b/apps/parent/build.gradle @@ -152,6 +152,8 @@ android { composeOptions { kotlinCompilerExtensionVersion = Versions.KOTLIN_COMPOSE_COMPILER_VERSION } + + testOptions.animationsDisabled = true } dependencies { diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxListInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxListInteractionTest.kt new file mode 100644 index 0000000000..b1d5a9aa9c --- /dev/null +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ParentInboxListInteractionTest.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.instructure.parentapp.ui.interaction + +import androidx.compose.ui.platform.ComposeView +import androidx.test.espresso.matcher.ViewMatchers +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils +import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck +import com.instructure.canvas.espresso.common.interaction.InboxListInteractionTest +import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockCanvas.addRecipientsToCourse +import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvasapi2.models.CanvasContextPermission +import com.instructure.canvasapi2.models.User +import com.instructure.parentapp.BuildConfig +import com.instructure.parentapp.features.login.LoginActivity +import com.instructure.parentapp.ui.pages.DashboardPage +import com.instructure.parentapp.util.ParentPrefs +import com.instructure.parentapp.utils.ParentActivityTestRule +import com.instructure.parentapp.utils.tokenLogin +import dagger.hilt.android.testing.HiltAndroidTest +import org.hamcrest.Matchers +import org.junit.Before + +@HiltAndroidTest +class ParentInboxListInteractionTest : InboxListInteractionTest() { + override val isTesting = BuildConfig.IS_TESTING + + override val activityRule = ParentActivityTestRule(LoginActivity::class.java) + + private val dashboardPage = DashboardPage() + + override fun goToInbox(data: MockCanvas) { + val parent = data.parents.first() + val token = data.tokenFor(parent)!! + tokenLogin(data.domain, token, parent) + + dashboardPage.assertPageObjects() + dashboardPage.openNavigationDrawer() + dashboardPage.clickInbox() + } + + override fun createInitialData(courseCount: Int): MockCanvas { + val data = MockCanvas.init( + parentCount = 1, + studentCount = 1, + courseCount = courseCount, + teacherCount = 1, + favoriteCourseCount = courseCount + ) + + val course1 = data.courses.values.first() + + data.addCoursePermissions( + course1.id, + CanvasContextPermission(send_messages_all = true, send_messages = true) + ) + + data.addRecipientsToCourse( + course = course1, + students = data.students, + teachers = data.teachers + ) + + return data + } + + override fun getLoggedInUser(): User { + return MockCanvas.data.parents.first() + } + + override fun getOtherUser(): User { + return MockCanvas.data.teachers.first() + } + + override fun enableAndConfigureAccessibilityChecks() { + extraAccessibilitySupressions = Matchers.allOf( + AccessibilityCheckResultUtils.matchesCheck( + SpeakableTextPresentCheck::class.java + ), + AccessibilityCheckResultUtils.matchesViews( + ViewMatchers.withParent( + ViewMatchers.withClassName( + Matchers.equalTo(ComposeView::class.java.name) + ) + ) + ) + ) + + super.enableAndConfigureAccessibilityChecks() + } +} \ No newline at end of file diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/DashboardPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/DashboardPage.kt index 37ba572767..849b72219c 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/DashboardPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/DashboardPage.kt @@ -24,7 +24,7 @@ import com.instructure.espresso.click import com.instructure.espresso.page.BasePage import com.instructure.espresso.page.getStringFromResource import com.instructure.espresso.page.onView -import com.instructure.espresso.page.onViewWithContentDescription +import com.instructure.espresso.page.onViewWithId import com.instructure.espresso.page.onViewWithText import com.instructure.espresso.page.plus import com.instructure.espresso.page.withAncestor @@ -43,7 +43,7 @@ class DashboardPage : BasePage(R.id.drawer_layout) { } fun openNavigationDrawer() { - onViewWithContentDescription(R.string.navigation_drawer_open).click() + onViewWithId(R.id.navigationButtonHolder).click() } fun assertSelectedStudent(name: String) { @@ -75,4 +75,8 @@ class DashboardPage : BasePage(R.id.drawer_layout) { fun tapSwitchUsers() { onViewWithText(R.string.navigationDrawerSwitchUsers).click() } + + fun clickInbox() { + onViewWithText(R.string.inbox).click() + } } diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTestExtensions.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTestExtensions.kt index 27d32e3fe5..dd9292fe80 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTestExtensions.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTestExtensions.kt @@ -17,11 +17,12 @@ package com.instructure.parentapp.utils +import com.instructure.canvas.espresso.CanvasTest import com.instructure.canvasapi2.models.User import com.instructure.parentapp.features.login.LoginActivity -fun ParentTest.tokenLogin(domain: String, token: String, user: User, assertDashboard: Boolean = true) { +fun CanvasTest.tokenLogin(domain: String, token: String, user: User, assertDashboard: Boolean = true) { activityRule.runOnUiThread { (originalActivity as LoginActivity).loginWithToken( token, @@ -30,5 +31,7 @@ fun ParentTest.tokenLogin(domain: String, token: String, user: User, assertDashb ) } - if (assertDashboard) dashboardPage.assertPageObjects() + if (assertDashboard && this is ParentTest) { + dashboardPage.assertPageObjects() + } } diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/DashboardModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/DashboardModule.kt index b3bdc2ab5c..4a84ae2ebb 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/di/DashboardModule.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/di/DashboardModule.kt @@ -18,7 +18,10 @@ package com.instructure.parentapp.di import com.instructure.canvasapi2.apis.EnrollmentAPI +import com.instructure.canvasapi2.apis.UnreadCountAPI import com.instructure.parentapp.features.dashboard.DashboardRepository +import com.instructure.parentapp.features.dashboard.InboxCountUpdater +import com.instructure.parentapp.features.dashboard.InboxCountUpdaterImpl import com.instructure.parentapp.features.dashboard.SelectedStudentHolder import com.instructure.parentapp.features.dashboard.SelectedStudentHolderImpl import dagger.Module @@ -34,9 +37,10 @@ class DashboardModule { @Provides fun provideDashboardRepository( - enrollmentApi: EnrollmentAPI.EnrollmentInterface + enrollmentApi: EnrollmentAPI.EnrollmentInterface, + unreadCountsApi: UnreadCountAPI.UnreadCountsInterface ): DashboardRepository { - return DashboardRepository(enrollmentApi) + return DashboardRepository(enrollmentApi, unreadCountsApi) } } @@ -49,4 +53,10 @@ class SelectedStudentHolderModule { fun provideSelectedStudentHolder(): SelectedStudentHolder { return SelectedStudentHolderImpl() } + + @Provides + @Singleton + fun provideInboxCountUpdater(): InboxCountUpdater { + return InboxCountUpdaterImpl() + } } diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardFragment.kt index bd812c3ecb..7f6cd97522 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardFragment.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardFragment.kt @@ -18,16 +18,23 @@ package com.instructure.parentapp.features.dashboard import android.content.Intent +import android.content.res.ColorStateList +import android.graphics.drawable.GradientDrawable import android.os.Bundle import android.util.Log +import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.core.os.BundleCompat import androidx.core.view.GravityCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.navigation.NavController import androidx.navigation.NavController.Companion.KEY_DEEP_LINK_INTENT @@ -39,7 +46,10 @@ import com.instructure.pandautils.utils.ColorKeeper import com.instructure.pandautils.utils.ViewStyler import com.instructure.pandautils.utils.animateCircularBackgroundColorChange import com.instructure.pandautils.utils.applyTheme +import com.instructure.pandautils.utils.getDrawableCompat +import com.instructure.pandautils.utils.onClick import com.instructure.pandautils.utils.showThemed +import com.instructure.pandautils.utils.toPx import com.instructure.parentapp.R import com.instructure.parentapp.databinding.FragmentDashboardBinding import com.instructure.parentapp.databinding.NavigationDrawerHeaderLayoutBinding @@ -65,6 +75,8 @@ class DashboardFragment : Fragment(), NavigationCallbacks { private lateinit var navController: NavController private lateinit var headerLayoutBinding: NavigationDrawerHeaderLayoutBinding + private var inboxBadge: TextView? = null + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -82,15 +94,32 @@ class DashboardFragment : Fragment(), NavigationCallbacks { setupNavigation() lifecycleScope.launch { - viewModel.data.collectLatest { + viewModel.data.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collectLatest { setupNavigationDrawerHeader(it.userViewData) setupAppColors(it.selectedStudent) + updateUnreadCount(it.unreadCount) } } handleDeeplink() } + private fun updateUnreadCount(unreadCount: Int) { + val unreadCountText = if (unreadCount <= 99) unreadCount.toString() else requireContext().getString(R.string.inboxUnreadCountMoreThan99) + inboxBadge?.visibility = if (unreadCount == 0) View.GONE else View.VISIBLE + inboxBadge?.text = unreadCountText + binding.unreadCountBadge.visibility = if (unreadCount == 0) View.GONE else View.VISIBLE + binding.unreadCountBadge.text = unreadCountText + + val navButtonContentDescription = if (unreadCount == 0) { + getString(R.string.navigation_drawer_open) + } else { + getString(R.string.a11y_parentOpenNavigationDrawerWithBadge, unreadCountText) + } + + binding.navigationButtonHolder.contentDescription = navButtonContentDescription + } + private fun setupNavigation() { val navHostFragment = childFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment navController = navHostFragment.navController @@ -118,10 +147,8 @@ class DashboardFragment : Fragment(), NavigationCallbacks { } private fun setupToolbar() { - val toolbar = binding.toolbar - toolbar.setNavigationIcon(R.drawable.ic_hamburger) - toolbar.navigationContentDescription = getString(R.string.navigation_drawer_open) - toolbar.setNavigationOnClickListener { + binding.navigationButtonHolder.contentDescription = getString(R.string.navigation_drawer_open) + binding.navigationButtonHolder.onClick { openNavigationDrawer() } } @@ -131,6 +158,21 @@ class DashboardFragment : Fragment(), NavigationCallbacks { headerLayoutBinding = NavigationDrawerHeaderLayoutBinding.bind(navView.getHeaderView(0)) + val actionView = (navView.menu.findItem(R.id.inbox)).actionView as LinearLayout + actionView.gravity = Gravity.CENTER + + inboxBadge = TextView(requireContext()) + actionView.addView(inboxBadge) + + inboxBadge?.width = 24.toPx + inboxBadge?.height = 24.toPx + inboxBadge?.gravity = Gravity.CENTER + inboxBadge?.textSize = 10f + inboxBadge?.setTextColor(requireContext().getColor(R.color.white)) + inboxBadge?.setBackgroundResource(R.drawable.bg_button_full_rounded_filled) + inboxBadge?.visibility = View.GONE + + navView.setNavigationItemSelectedListener { closeNavigationDrawer() when (it.itemId) { @@ -186,8 +228,14 @@ class DashboardFragment : Fragment(), NavigationCallbacks { } else { binding.toolbar.animateCircularBackgroundColorChange(color, binding.toolbarImage) } + inboxBadge?.backgroundTintList = ColorStateList(arrayOf(intArrayOf()), intArrayOf(color)) binding.bottomNav.applyTheme(color, requireActivity().getColor(R.color.textDarkest)) ViewStyler.setStatusBarDark(requireActivity(), color) + + val gradientDrawable = requireContext().getDrawableCompat(R.drawable.bg_button_full_rounded_filled_with_border) as? GradientDrawable + gradientDrawable?.setStroke(2.toPx, color) + binding.unreadCountBadge.background = gradientDrawable + binding.unreadCountBadge.setTextColor(color) } private fun openNavigationDrawer() { diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardRepository.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardRepository.kt index cba4e8fc2a..50691ad9e0 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardRepository.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardRepository.kt @@ -18,13 +18,16 @@ package com.instructure.parentapp.features.dashboard import com.instructure.canvasapi2.apis.EnrollmentAPI +import com.instructure.canvasapi2.apis.UnreadCountAPI import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.models.User import com.instructure.canvasapi2.utils.depaginate +import com.instructure.pandautils.utils.orDefault class DashboardRepository( - private val enrollmentApi: EnrollmentAPI.EnrollmentInterface + private val enrollmentApi: EnrollmentAPI.EnrollmentInterface, + private val unreadCountApi: UnreadCountAPI.UnreadCountsInterface ) { suspend fun getStudents(): List { @@ -37,4 +40,10 @@ class DashboardRepository( .distinct() .sortedBy { it.sortableName } } + + suspend fun getUnreadCounts(): Int { + val params = RestParams(isForceReadFromNetwork = true) + val unreadCount = unreadCountApi.getUnreadConversationCount(params).dataOrNull?.unreadCount ?: "0" + return unreadCount.toIntOrNull().orDefault() + } } diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewData.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewData.kt index 24d6464cc8..93313c2573 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewData.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewData.kt @@ -24,7 +24,8 @@ data class DashboardViewData( val userViewData: UserViewData? = null, val studentSelectorExpanded: Boolean = false, val studentItems: List = emptyList(), - val selectedStudent: User? = null + val selectedStudent: User? = null, + val unreadCount: Int = 0 ) data class StudentItemViewData( diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewModel.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewModel.kt index 3c45498294..7d8535d17a 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewModel.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewModel.kt @@ -45,7 +45,8 @@ class DashboardViewModel @Inject constructor( private val previousUsersUtils: PreviousUsersUtils, private val apiPrefs: ApiPrefs, private val parentPrefs: ParentPrefs, - private val selectedStudentHolder: SelectedStudentHolder + private val selectedStudentHolder: SelectedStudentHolder, + private val inboxCountUpdater: InboxCountUpdater ) : ViewModel() { private val _data = MutableStateFlow(DashboardViewData()) @@ -61,12 +62,21 @@ class DashboardViewModel @Inject constructor( } private fun loadData() { + viewModelScope.launch { + inboxCountUpdater.shouldRefreshInboxCountFlow.collect {shouldUpdate -> + if (shouldUpdate) { + updateUnreadCount() + inboxCountUpdater.updateShouldRefreshInboxCount(false) + } + } + } + viewModelScope.tryLaunch { _state.value = ViewState.Loading setupUserInfo() - loadStudents() + updateUnreadCount() if (_data.value.studentItems.isEmpty()) { _state.value = ViewState.Empty( @@ -152,6 +162,15 @@ class DashboardViewModel @Inject constructor( } } + private suspend fun updateUnreadCount() { + val unreadCount = repository.getUnreadCounts() + _data.update { data -> + data.copy( + unreadCount = unreadCount + ) + } + } + fun toggleStudentSelector() { _data.update { it.copy(studentSelectorExpanded = !it.studentSelectorExpanded) diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/InboxCountUpdater.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/InboxCountUpdater.kt new file mode 100644 index 0000000000..e23eb39335 --- /dev/null +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/InboxCountUpdater.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.parentapp.features.dashboard + +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow + +interface InboxCountUpdater { + val shouldRefreshInboxCountFlow: SharedFlow + suspend fun updateShouldRefreshInboxCount(shouldRefresh: Boolean) +} + +class InboxCountUpdaterImpl : InboxCountUpdater { + private val _shouldRefreshInboxCountFlow = MutableSharedFlow(replay = 1) + override val shouldRefreshInboxCountFlow = _shouldRefreshInboxCountFlow.asSharedFlow() + + override suspend fun updateShouldRefreshInboxCount(shouldRefresh: Boolean) { + _shouldRefreshInboxCountFlow.emit(shouldRefresh) + } +} \ No newline at end of file diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/inbox/list/ParentInboxRepository.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/inbox/list/ParentInboxRepository.kt index 3a49c00daf..42b1c7c5d0 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/inbox/list/ParentInboxRepository.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/inbox/list/ParentInboxRepository.kt @@ -23,6 +23,7 @@ import com.instructure.canvasapi2.apis.InboxApi import com.instructure.canvasapi2.apis.ProgressAPI import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.Enrollment import com.instructure.canvasapi2.utils.DataResult import com.instructure.canvasapi2.utils.depaginate import com.instructure.canvasapi2.utils.hasActiveEnrollment @@ -37,7 +38,7 @@ class ParentInboxRepository( ) : InboxRepository(inboxApi, groupsApi, progressApi) { override suspend fun getCourses(params: RestParams): DataResult> { - val coursesResult = coursesApi.getFirstPageCourses(params) + val coursesResult = coursesApi.getCoursesByEnrollmentType(Enrollment.EnrollmentType.Observer.apiTypeString, params) .depaginate { nextUrl -> coursesApi.next(nextUrl, params) } if (coursesResult.isFail) return coursesResult diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/main/MainActivity.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/main/MainActivity.kt index 803ea8fcef..b794d87545 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/main/MainActivity.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/main/MainActivity.kt @@ -23,27 +23,34 @@ import android.net.Uri import android.os.Bundle import android.util.Log import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment import com.instructure.pandautils.binding.viewBinding +import com.instructure.pandautils.features.inbox.list.OnUnreadCountInvalidated import com.instructure.pandautils.interfaces.NavigationCallbacks import com.instructure.pandautils.utils.Const import com.instructure.parentapp.R import com.instructure.parentapp.databinding.ActivityMainBinding +import com.instructure.parentapp.features.dashboard.InboxCountUpdater import com.instructure.parentapp.features.splash.SplashFragment import com.instructure.parentapp.util.navigation.Navigation import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch import javax.inject.Inject @AndroidEntryPoint -class MainActivity : AppCompatActivity() { +class MainActivity : AppCompatActivity(), OnUnreadCountInvalidated { private val binding by viewBinding(ActivityMainBinding::inflate) @Inject lateinit var navigation: Navigation + @Inject + lateinit var inboxCountUpdater: InboxCountUpdater + private lateinit var navController: NavController override fun onCreate(savedInstanceState: Bundle?) { @@ -93,10 +100,16 @@ class MainActivity : AppCompatActivity() { } } + override fun invalidateUnreadCount() { + lifecycleScope.launch { + inboxCountUpdater.updateShouldRefreshInboxCount(true) + } + } + companion object { fun createIntent(context: Context, uri: Uri): Intent { val intent = Intent(context, MainActivity::class.java) - intent.setData(uri) + intent.data = uri return intent } diff --git a/apps/parent/src/main/res/layout/fragment_dashboard.xml b/apps/parent/src/main/res/layout/fragment_dashboard.xml index 3e21be83f1..41fd89a64d 100644 --- a/apps/parent/src/main/res/layout/fragment_dashboard.xml +++ b/apps/parent/src/main/res/layout/fragment_dashboard.xml @@ -60,52 +60,90 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> - - - - - + + + + + + + + + + - - - - + android:layout_gravity="center" + android:layout_marginBottom="12dp"> + + + + + + + + + + diff --git a/apps/parent/src/main/res/menu/nav_drawer.xml b/apps/parent/src/main/res/menu/nav_drawer.xml index 0afd87489e..c7006e295e 100644 --- a/apps/parent/src/main/res/menu/nav_drawer.xml +++ b/apps/parent/src/main/res/menu/nav_drawer.xml @@ -13,13 +13,15 @@ ~ You should have received a copy of the GNU General Public License ~ along with this program. If not, see . --> - + + android:title="@string/screenTitleInbox" + app:actionViewClass="android.widget.LinearLayout"/> () + private val inboxCountUpdater: InboxCountUpdater = TestInboxCountUpdater(inboxCountUpdaterFlow) private lateinit var viewModel: DashboardViewModel @@ -92,7 +97,7 @@ class DashboardViewModelTest { createViewModel() - Assert.assertEquals(expected, viewModel.data.value.userViewData) + assertEquals(expected, viewModel.data.value.userViewData) } @Test @@ -111,7 +116,7 @@ class DashboardViewModelTest { StudentItemViewData(2L, "Student Two", "avatar2") ) - Assert.assertEquals(expected, viewModel.data.value.studentItems.map { it.studentItemViewData }) + assertEquals(expected, viewModel.data.value.studentItems.map { it.studentItemViewData }) } @Test @@ -126,7 +131,7 @@ class DashboardViewModelTest { R.drawable.panda_manage_students ) - Assert.assertEquals(expected, viewModel.state.value) + assertEquals(expected, viewModel.state.value) } @Test @@ -151,7 +156,7 @@ class DashboardViewModelTest { createViewModel() - Assert.assertEquals(expected, viewModel.data.value.selectedStudent) + assertEquals(expected, viewModel.data.value.selectedStudent) } @Test @@ -165,12 +170,12 @@ class DashboardViewModelTest { createViewModel() - Assert.assertEquals(students.first(), viewModel.data.value.selectedStudent) + assertEquals(students.first(), viewModel.data.value.selectedStudent) viewModel.data.value.studentItems.last().onStudentClick() - Assert.assertEquals(students.last(), viewModel.data.value.selectedStudent) - Assert.assertFalse(viewModel.data.value.studentSelectorExpanded) + assertEquals(students.last(), viewModel.data.value.selectedStudent) + assertFalse(viewModel.data.value.studentSelectorExpanded) coVerify { selectedStudentHolder.updateSelectedStudent(students.last()) } } @@ -179,10 +184,26 @@ class DashboardViewModelTest { createViewModel() viewModel.toggleStudentSelector() - Assert.assertTrue(viewModel.data.value.studentSelectorExpanded) + assertTrue(viewModel.data.value.studentSelectorExpanded) viewModel.toggleStudentSelector() - Assert.assertFalse(viewModel.data.value.studentSelectorExpanded) + assertFalse(viewModel.data.value.studentSelectorExpanded) + } + + @Test + fun `Update unread count when the update unread count flow triggers an update`() = runTest { + val students = listOf(User(id = 1L), User(id = 2L)) + coEvery { repository.getStudents() } returns students + coEvery { repository.getUnreadCounts() } returns 0 + + createViewModel() + + assertEquals(0, viewModel.data.value.unreadCount) + + coEvery { repository.getUnreadCounts() } returns 1 + inboxCountUpdaterFlow.emit(true) + + assertEquals(1, viewModel.data.value.unreadCount) } private fun createViewModel() { @@ -192,7 +213,8 @@ class DashboardViewModelTest { previousUsersUtils = previousUsersUtils, apiPrefs = apiPrefs, parentPrefs = parentPrefs, - selectedStudentHolder = selectedStudentHolder + selectedStudentHolder = selectedStudentHolder, + inboxCountUpdater = inboxCountUpdater ) } } diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/TestInboxCountUpdater.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/TestInboxCountUpdater.kt new file mode 100644 index 0000000000..1c61105423 --- /dev/null +++ b/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/TestInboxCountUpdater.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.parentapp.features.dashboard + +import kotlinx.coroutines.flow.MutableSharedFlow + +class TestInboxCountUpdater( + override val shouldRefreshInboxCountFlow: MutableSharedFlow +) : InboxCountUpdater { + override suspend fun updateShouldRefreshInboxCount(shouldRefresh: Boolean) { + shouldRefreshInboxCountFlow.emit(shouldRefresh) + } +} \ No newline at end of file diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/inbox/list/ParentInboxRepositoryTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/inbox/list/ParentInboxRepositoryTest.kt new file mode 100644 index 0000000000..bd984d65d1 --- /dev/null +++ b/apps/parent/src/test/java/com/instructure/parentapp/features/inbox/list/ParentInboxRepositoryTest.kt @@ -0,0 +1,63 @@ +package com.instructure.parentapp.features.inbox.list/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.EnrollmentAPI +import com.instructure.canvasapi2.apis.GroupAPI +import com.instructure.canvasapi2.apis.InboxApi +import com.instructure.canvasapi2.apis.ProgressAPI +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.Enrollment +import com.instructure.canvasapi2.utils.DataResult +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Test + +class ParentInboxRepositoryTest { + + private val inboxApi: InboxApi.InboxInterface = mockk(relaxed = true) + private val coursesApi: CourseAPI.CoursesInterface = mockk(relaxed = true) + private val groupsApi: GroupAPI.GroupInterface = mockk(relaxed = true) + private val progressApi: ProgressAPI.ProgressInterface = mockk(relaxed = true) + + private val inboxRepository = + ParentInboxRepository(inboxApi, coursesApi, groupsApi, progressApi) + + @Test + fun `Get contexts returns only valid courses`() = runTest { + val courses = listOf( + Course(44, enrollments = mutableListOf(Enrollment(enrollmentState = EnrollmentAPI.STATE_ACTIVE))), + Course(11) // no active enrollment + ) + coEvery { coursesApi.getCoursesByEnrollmentType(any(), any()) } returns DataResult.Success(courses) + coEvery { groupsApi.getFirstPageGroups(any()) } returns DataResult.Success(emptyList()) + + val canvasContextsResults = inboxRepository.getCanvasContexts() + + assertEquals(1, canvasContextsResults.dataOrNull!!.size) + assertEquals(courses[0].id, canvasContextsResults.dataOrNull!![0].id) + } + + @Test + fun `Get contexts returns failed results when request fails`() = runTest { + coEvery { coursesApi.getCoursesByEnrollmentType(any(), any()) } returns DataResult.Fail() + + val canvasContextsResults = inboxRepository.getCanvasContexts() + + assertEquals(DataResult.Fail(), canvasContextsResults) + } +} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt index 7ee14b2b5e..89410e1dae 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt @@ -76,10 +76,10 @@ class InboxE2ETest: StudentTest() { Log.d(STEP_TAG,"Refresh the page. Assert that there is a conversation and it is the previously seeded one.") refresh() inboxPage.assertHasConversation() - inboxPage.assertConversationDisplayed(seededConversation) + inboxPage.assertConversationDisplayed(seededConversation.subject) Log.d(STEP_TAG,"Select '${seededConversation.subject}' conversation. Assert that is has not been starred already.") - inboxPage.openConversation(seededConversation) + inboxPage.openConversation(seededConversation.subject) inboxConversationPage.assertNotStarred() Log.d(STEP_TAG,"Toggle Starred to mark '${seededConversation.subject}' conversation as favourite. Assert that it has became starred.") @@ -92,28 +92,28 @@ class InboxE2ETest: StudentTest() { Log.d(STEP_TAG,"Select '${seededConversation.subject}' conversation. Mark as Unread by clicking on the 'More Options' menu, 'Mark as Unread' menu point.") inboxPage.assertUnreadMarkerVisibility(seededConversation.subject, ViewMatchers.Visibility.GONE) - inboxPage.openConversation(seededConversation) + inboxPage.openConversation(seededConversation.subject) inboxConversationPage.markUnread() //After select 'Mark as Unread', we will be navigated back to Inbox Page Log.d(STEP_TAG,"Assert that '${seededConversation.subject}' conversation has been marked as unread.") inboxPage.assertUnreadMarkerVisibility(seededConversation.subject, ViewMatchers.Visibility.VISIBLE) Log.d(STEP_TAG,"Select '${seededConversation.subject}' conversation. Archive it by clicking on the 'More Options' menu, 'Archive' menu point.") - inboxPage.openConversation(seededConversation) + inboxPage.openConversation(seededConversation.subject) inboxConversationPage.archive() //After select 'Archive', we will be navigated back to Inbox Page Log.d(STEP_TAG,"Assert that '${seededConversation.subject}' conversation has removed from 'All' tab.") //TODO: Discuss this logic if it's ok if we don't show Archived messages on 'All' tab... - inboxPage.assertConversationNotDisplayed(seededConversation) + inboxPage.assertConversationNotDisplayed(seededConversation.subject) Log.d(STEP_TAG,"Select 'Archived' conversation filter.") inboxPage.filterInbox("Archived") Log.d(STEP_TAG,"Assert that '${seededConversation.subject}' conversation is displayed by the 'Archived' filter.") - inboxPage.assertConversationDisplayed(seededConversation) + inboxPage.assertConversationDisplayed(seededConversation.subject) Log.d(STEP_TAG, "Select '${seededConversation.subject}' conversation. Assert that the selected number of conversations on the toolbar is 1." + "Unarchive it, and assert that it is not displayed in the 'ARCHIVED' scope any more.") - inboxPage.selectConversation(seededConversation) + inboxPage.selectConversation(seededConversation.subject) inboxPage.assertSelectedConversationNumber("1") inboxPage.clickUnArchive() inboxPage.assertInboxEmpty() @@ -216,7 +216,7 @@ class InboxE2ETest: StudentTest() { Log.d(STEP_TAG,"Refresh the page. Assert that there is a conversation and it is the previously seeded one.") refresh() inboxPage.assertHasConversation() - inboxPage.assertConversationDisplayed(seededConversation) + inboxPage.assertConversationDisplayed(seededConversation.subject) Log.d(STEP_TAG,"Click on 'New Message' button.") inboxPage.pressNewMessageButton() @@ -243,7 +243,7 @@ class InboxE2ETest: StudentTest() { sleep(2000) // Allow time for messages to propagate Log.d(STEP_TAG,"Navigate back to Dashboard Page.") - inboxPage.goToDashboard() + dashboardPage.goToDashboard() dashboardPage.waitForRender() Log.d(STEP_TAG,"Log out with '${student1.name}' student.") @@ -255,7 +255,7 @@ class InboxE2ETest: StudentTest() { Log.d(STEP_TAG,"Open Inbox Page. Assert that both, the previously seeded 'normal' conversation and the group conversation are displayed.") dashboardPage.clickInboxTab() - inboxPage.assertConversationDisplayed(seededConversation) + inboxPage.assertConversationDisplayed(seededConversation.subject) inboxPage.assertConversationDisplayed(newMessageSubject) inboxPage.assertConversationDisplayed("Group Message") @@ -272,7 +272,7 @@ class InboxE2ETest: StudentTest() { Log.d(STEP_TAG,"Delete the whole '$newGroupMessageSubject' subject and assert that it has been removed from the conversation list on the Inbox Page.") inboxConversationPage.deleteConversation() //After deletion we will be navigated back to Inbox Page inboxPage.assertConversationNotDisplayed(newMessageSubject) - inboxPage.assertConversationDisplayed(seededConversation) + inboxPage.assertConversationDisplayed(seededConversation.subject) inboxPage.assertConversationDisplayed("Group Message") Log.d(STEP_TAG, "Navigate to 'INBOX' scope and select '$newGroupMessageSubject' conversation.") @@ -320,19 +320,19 @@ class InboxE2ETest: StudentTest() { Log.d(STEP_TAG,"Refresh the page. Assert that there is a conversation and it is the previously seeded one.") refresh() inboxPage.assertHasConversation() - inboxPage.assertConversationDisplayed(seededConversation) + inboxPage.assertConversationDisplayed(seededConversation.subject) Log.d(STEP_TAG, "Select '${seededConversation.subject}' conversation and swipe it right to make it read. Assert that the conversation became read.") inboxPage.selectConversation(seededConversation.subject) - inboxPage.swipeConversationRight(seededConversation) + inboxPage.swipeConversationRight(seededConversation.subject) inboxPage.assertUnreadMarkerVisibility(seededConversation.subject, ViewMatchers.Visibility.GONE) Log.d(STEP_TAG, "Select '${seededConversation.subject}' conversation and swipe it right again to make it unread. Assert that the conversation became unread.") - inboxPage.swipeConversationRight(seededConversation) + inboxPage.swipeConversationRight(seededConversation.subject) inboxPage.assertUnreadMarkerVisibility(seededConversation.subject, ViewMatchers.Visibility.VISIBLE) Log.d(STEP_TAG, "Swipe '${seededConversation.subject}' left and assert it is removed from the 'INBOX' scope because it has became archived.") - inboxPage.swipeConversationLeft(seededConversation) + inboxPage.swipeConversationLeft(seededConversation.subject) inboxPage.assertConversationNotDisplayed(seededConversation.subject) Log.d(STEP_TAG, "Navigate to 'ARCHIVED' scope. Assert that the '${seededConversation.subject}' conversation is displayed in the 'ARCHIVED' scope.") @@ -340,7 +340,7 @@ class InboxE2ETest: StudentTest() { inboxPage.assertConversationDisplayed(seededConversation.subject) Log.d(STEP_TAG, "Swipe '${seededConversation.subject}' left and assert it is removed from the 'ARCHIVED' scope because it has became unarchived.") - inboxPage.swipeConversationLeft(seededConversation) + inboxPage.swipeConversationLeft(seededConversation.subject) inboxPage.assertConversationNotDisplayed(seededConversation.subject) Log.d(STEP_TAG, "Navigate to 'INBOX' scope. Assert that the '${seededConversation.subject}' conversation is displayed in the 'INBOX' scope.") @@ -362,7 +362,7 @@ class InboxE2ETest: StudentTest() { } Log.d(STEP_TAG, "Swipe '${seededConversation.subject}' left and assert it is removed from the 'STARRED' scope because it has became unstarred.") - inboxPage.swipeConversationLeft(seededConversation) + inboxPage.swipeConversationLeft(seededConversation.subject) inboxPage.assertConversationNotDisplayed(seededConversation.subject) Log.d(STEP_TAG, "Navigate to 'UNREAD' scope. Assert that the conversation is displayed in the 'Unread' scope.") diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt index 8392395b13..baf444bf42 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DashboardInteractionTest.kt @@ -63,7 +63,7 @@ class DashboardInteractionTest : StudentTest() { val data = setUpData(courseCount = 1, favoriteCourseCount = 1) goToDashboard(data) dashboardPage.clickInboxTab() - inboxPage.goToDashboard() + dashboardPage.goToDashboard() dashboardPage.assertDisplaysCourse(data.courses.values.first()) // disambiguates via isDisplayed() // These get confused by the existence of multiple DashboardPages in the layout diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryDashboardInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryDashboardInteractionTest.kt index e1762cd4be..c77a537379 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryDashboardInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ElementaryDashboardInteractionTest.kt @@ -40,7 +40,7 @@ class ElementaryDashboardInteractionTest : StudentTest() { goToElementaryDashboard(courseCount = 1, favoriteCourseCount = 1) elementaryDashboardPage.assertPageObjects() elementaryDashboardPage.clickOnBottomNavigationBarInbox() - inboxPage.goToDashboard() + dashboardPage.goToDashboard() elementaryDashboardPage.assertToolbarTitle() elementaryDashboardPage.assertElementaryTabVisibleAndSelected(ElementaryDashboardPage.ElementaryTabType.HOMEROOM) } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InboxInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InboxConversationInteractionTest.kt similarity index 53% rename from apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InboxInteractionTest.kt rename to apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InboxConversationInteractionTest.kt index bf6fbcdcec..e399c0c5ac 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InboxInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/InboxConversationInteractionTest.kt @@ -24,11 +24,9 @@ import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.mockCanvas.MockCanvas import com.instructure.canvas.espresso.mockCanvas.addConversation import com.instructure.canvas.espresso.mockCanvas.addConversations -import com.instructure.canvas.espresso.mockCanvas.addConversationsToCourseMap import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions import com.instructure.canvas.espresso.mockCanvas.addRecipientsToCourse import com.instructure.canvas.espresso.mockCanvas.addSentConversation -import com.instructure.canvas.espresso.mockCanvas.createBasicConversation import com.instructure.canvas.espresso.mockCanvas.init import com.instructure.canvasapi2.models.Attachment import com.instructure.canvasapi2.models.CanvasContextPermission @@ -41,7 +39,7 @@ import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test @HiltAndroidTest -class InboxInteractionTest : StudentTest() { +class InboxConversationInteractionTest : StudentTest() { override fun displaysPageObjects() = Unit // Not used for interaction tests @Test @@ -222,85 +220,6 @@ class InboxInteractionTest : StudentTest() { inboxConversationPage.assertAttachmentDisplayed(attachmentName) } - @Test - @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun testInbox_filterMessagesByTypeAll() { - // Should be able to filter messages by All - val data = createInitialData() - data.addConversations(userId = student1.id, messageBody = "Short body") - val conversation = getFirstConversation(data) - dashboardPage.clickInboxTab() - inboxPage.assertConversationDisplayed(conversation.subject!!) - } - - @Test - @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun testInbox_filterMessagesByTypeUnread() { - // Should be able to filter messages by Unread - val data = createInitialData() - data.addConversations(userId = student1.id, messageBody = "Short body") - dashboardPage.clickInboxTab() - val conversation = data.conversations.values.first { - it.workflowState == Conversation.WorkflowState.UNREAD - } - inboxPage.filterInbox("Unread") - inboxPage.assertConversationDisplayed(conversation.subject!!) - } - - @Test - @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun testInbox_filterMessagesByTypeStarred() { - // Should be able to filter messages by Starred - val data = createInitialData() - data.addConversations(userId = student1.id, messageBody = "Short body") - dashboardPage.clickInboxTab() - val conversation = data.conversations.values.first { - it.isStarred - } - inboxPage.filterInbox("Starred") - inboxPage.assertConversationDisplayed(conversation.subject!!) - } - - @Test - @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun testInbox_filterMessagesByTypeSend() { - // Should be able to filter messages by Send - val data = createInitialData() - data.addConversations(userId = student1.id, messageBody = "Short body") - dashboardPage.clickInboxTab() - val conversation = data.conversations.values.first { - it.workflowState == Conversation.WorkflowState.UNREAD - } - inboxPage.filterInbox("Sent") - inboxPage.assertConversationDisplayed(conversation.subject!!) - } - - @Test - @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun testInbox_filterMessagesByTypeArchived() { - // Should be able to filter messages by Archived - val data = createInitialData() - data.addConversations(userId = student1.id, messageBody = "Short body") - dashboardPage.clickInboxTab() - val conversation = data.conversations.values.first { - it.workflowState == Conversation.WorkflowState.ARCHIVED - } - inboxPage.filterInbox("Archived") - inboxPage.assertConversationDisplayed(conversation.subject!!) - } - - @Test - @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun testInbox_filterMessagesByContext() { - // Should be able to filter messages by course or group - val data = createInitialData(courseCount = 2) - data.addConversationsToCourseMap(student1.id, data.courses.values.toList(), messageBody = "Short body") - val conversation = data.conversationCourseMap[course1.id]!!.first() - dashboardPage.clickInboxTab() - inboxPage.selectInboxFilter(course1) - inboxPage.assertConversationDisplayed(conversation.subject!!) - } - @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) fun testInbox_canComposeAndSendToRoleGroupsIfPermissionEnabled() { @@ -495,348 +414,6 @@ class InboxInteractionTest : StudentTest() { inboxConversationPage.assertMessageNotDisplayed(replyMessage) } - @Test - @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun testInbox_showEditToolbarWhenConversationIsSelected() { - val data = createInitialData() - val conversation = data.addConversation( - senderId = data.teachers.first().id, - receiverIds = listOf(data.students.first().id), - messageBody = "Body", - messageSubject = "Subject") - dashboardPage.clickInboxTab() - inboxPage.selectConversation(conversation) - inboxPage.assertEditToolbarIs(ViewMatchers.Visibility.VISIBLE) - inboxPage.assertSelectedConversationNumber("1") - } - - @Test - @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun testInbox_archiveMultipleConversations() { - val data = createInitialData() - data.addConversations(userId = student1.id, messageBody = "Short body") - val conversation1 = data.addConversation( - senderId = data.teachers.first().id, - receiverIds = listOf(data.students.first().id), - messageBody = "Body", - messageSubject = "Subject") - val conversation2 = data.addConversation( - senderId = data.teachers.first().id, - receiverIds = listOf(data.students.first().id), - messageBody = "Body 2", - messageSubject = "Subject 2") - dashboardPage.clickInboxTab() - inboxPage.selectConversations(listOf(conversation1.subject!!, conversation2.subject!!)) - inboxPage.clickArchive() - inboxPage.assertConversationNotDisplayed(conversation1.subject!!) - inboxPage.assertConversationNotDisplayed(conversation2.subject!!) - inboxPage.assertEditToolbarIs(ViewMatchers.Visibility.GONE) - - inboxPage.filterInbox("Archived") - inboxPage.assertConversationDisplayed(conversation1.subject!!) - inboxPage.assertConversationDisplayed(conversation2.subject!!) - } - - @Test - @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun testInbox_starMultipleConversations() { - val data = createInitialData() - val conversation1 = data.addConversation( - senderId = data.teachers.first().id, - receiverIds = listOf(data.students.first().id), - messageBody = "Body", - messageSubject = "Subject") - val conversation2 = data.addConversation( - senderId = data.teachers.first().id, - receiverIds = listOf(data.students.first().id), - messageBody = "Body 2", - messageSubject = "Subject 2") - data.addConversations(userId = student1.id, messageBody = "Short body") - dashboardPage.clickInboxTab() - inboxPage.selectConversations(listOf(conversation1.subject!!, conversation2.subject!!)) - inboxPage.clickStar() - inboxPage.assertConversationStarred(conversation1.subject!!) - inboxPage.assertConversationStarred(conversation2.subject!!) - inboxPage.assertEditToolbarIs(ViewMatchers.Visibility.VISIBLE) - } - - @Test - @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun testInbox_unstarMultipleConversations() { - val data = createInitialData() - data.addConversations(userId = student1.id, messageBody = "Short body") - val conversation1 = data.addConversation( - senderId = data.teachers.first().id, - receiverIds = listOf(data.students.first().id), - messageBody = "Body", - messageSubject = "Subject") - data.conversations[conversation1.id] = conversation1.copy(isStarred = true) - val conversation2 = data.addConversation( - senderId = data.teachers.first().id, - receiverIds = listOf(data.students.first().id), - messageBody = "Body 2", - messageSubject = "Subject 2") - data.conversations[conversation2.id] = conversation2.copy(isStarred = true) - - dashboardPage.clickInboxTab() - inboxPage.filterInbox("Starred") - inboxPage.selectConversations(listOf(conversation1.subject!!, conversation2.subject!!)) - inboxPage.assertSelectedConversationNumber("2") - inboxPage.clickUnstar() - inboxPage.assertConversationNotDisplayed(conversation1.subject!!) - inboxPage.assertConversationNotDisplayed(conversation2.subject!!) - inboxPage.assertEditToolbarIs(ViewMatchers.Visibility.GONE) - } - - @Test - @TestMetaData(Priority.COMMON, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun testInbox_starMultipleConversationWithDifferentStates() { - val data = createInitialData() - val conversation1 = data.addConversation( - senderId = data.teachers.first().id, - receiverIds = listOf(data.students.first().id), - messageBody = "Body", - messageSubject = "Subject") - data.conversations[conversation1.id] = conversation1.copy(isStarred = true) - val conversation2 = data.addConversation( - senderId = data.teachers.first().id, - receiverIds = listOf(data.students.first().id), - messageBody = "Body 2", - messageSubject = "Subject 2") - data.conversations[conversation2.id] = conversation2.copy(isStarred = false) - data.addConversations(userId = student1.id, messageBody = "Short body") - - dashboardPage.clickInboxTab() - inboxPage.selectConversations(listOf(conversation1.subject!!, conversation2.subject!!)) - inboxPage.assertSelectedConversationNumber("2") - - inboxPage.assertStarDisplayed() - inboxPage.clickStar() - - inboxPage.assertConversationStarred(conversation1.subject!!) - inboxPage.assertConversationStarred(conversation2.subject!!) - inboxPage.assertSelectedConversationNumber("2") - inboxPage.assertEditToolbarIs(ViewMatchers.Visibility.VISIBLE) - - inboxPage.assertUnStarDisplayed() - inboxPage.clickUnstar() - - inboxPage.assertConversationNotStarred(conversation1.subject!!) - inboxPage.assertConversationNotStarred(conversation2.subject!!) - } - - @Test - @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun testInbox_markAsReadUnreadMultipleConversations() { - val data = createInitialData() - data.addConversations(userId = student1.id, messageBody = "Short body") - val conversation1 = data.addConversation( - senderId = data.teachers.first().id, - receiverIds = listOf(data.students.first().id), - messageBody = "Body", - messageSubject = "Subject") - val conversation2 = data.addConversation( - senderId = data.teachers.first().id, - receiverIds = listOf(data.students.first().id), - messageBody = "Body 2", - messageSubject = "Subject 2") - - dashboardPage.clickInboxTab() - - inboxPage.selectConversations(listOf(conversation1.subject!!, conversation2.subject!!)) - inboxPage.assertSelectedConversationNumber("2") - - inboxPage.clickMarkAsRead() - inboxPage.assertUnreadMarkerVisibility(conversation1.subject!!, ViewMatchers.Visibility.GONE) - inboxPage.assertUnreadMarkerVisibility(conversation2.subject!!, ViewMatchers.Visibility.GONE) - inboxPage.assertEditToolbarIs(ViewMatchers.Visibility.VISIBLE) - - inboxPage.clickMarkAsUnread() - inboxPage.assertUnreadMarkerVisibility(conversation1.subject!!, ViewMatchers.Visibility.VISIBLE) - inboxPage.assertUnreadMarkerVisibility(conversation2.subject!!, ViewMatchers.Visibility.VISIBLE) - inboxPage.assertEditToolbarIs(ViewMatchers.Visibility.VISIBLE) - - inboxPage.selectConversation(conversation1) - inboxPage.clickMarkAsRead() - - inboxPage.assertUnreadMarkerVisibility(conversation1.subject!!, ViewMatchers.Visibility.VISIBLE) - inboxPage.assertUnreadMarkerVisibility(conversation2.subject!!, ViewMatchers.Visibility.GONE) - inboxPage.assertEditToolbarIs(ViewMatchers.Visibility.VISIBLE) - - inboxPage.selectConversation(conversation1) - inboxPage.clickMarkAsRead() - - inboxPage.assertUnreadMarkerVisibility(conversation1.subject!!, ViewMatchers.Visibility.GONE) - inboxPage.assertUnreadMarkerVisibility(conversation2.subject!!, ViewMatchers.Visibility.GONE) - } - - @Test - @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun testInbox_deleteMultipleConversations() { - val data = createInitialData() - data.addConversations(userId = student1.id, messageBody = "Short body") - val conversation1 = data.addConversation( - senderId = data.teachers.first().id, - receiverIds = listOf(data.students.first().id), - messageBody = "Body", - messageSubject = "Subject") - val conversation2 = data.addConversation( - senderId = data.teachers.first().id, - receiverIds = listOf(data.students.first().id), - messageBody = "Body 2", - messageSubject = "Subject 2") - - dashboardPage.clickInboxTab() - - inboxPage.selectConversations(listOf(conversation1.subject!!, conversation2.subject!!)) - inboxPage.assertSelectedConversationNumber("2") - - inboxPage.clickDelete() - inboxPage.confirmDelete() - - inboxPage.assertConversationNotDisplayed(conversation1.subject!!) - inboxPage.assertConversationNotDisplayed(conversation2.subject!!) - inboxPage.assertEditToolbarIs(ViewMatchers.Visibility.GONE) - } - - @Test - @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun testInbox_swipeToReadUnread() { - val data = createInitialData() - data.addConversations(userId = student1.id, messageBody = "Short body") - val conversation = data.addConversation( - senderId = data.teachers.first().id, - receiverIds = listOf(data.students.first().id), - messageBody = "Body", - messageSubject = "Subject") - - dashboardPage.clickInboxTab() - inboxPage.swipeConversationRight(conversation) - inboxPage.assertUnreadMarkerVisibility(conversation.subject!!, ViewMatchers.Visibility.GONE) - - inboxPage.swipeConversationRight(conversation) - inboxPage.assertUnreadMarkerVisibility(conversation.subject!!, ViewMatchers.Visibility.VISIBLE) - } - - @Test - @TestMetaData(Priority.COMMON, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun testInbox_swipeGesturesInUnreadScope() { - val data = createInitialData() - val conversation = data.addConversation( - senderId = data.teachers.first().id, - receiverIds = listOf(data.students.first().id), - messageBody = "Unread body", - messageSubject = "Unread Subject") - val unreadConversation = data.createBasicConversation(data.teachers.first().id, workflowState = Conversation.WorkflowState.UNREAD, messageBody = "Unread Body 2") - data.conversations[unreadConversation.id] = unreadConversation - - dashboardPage.clickInboxTab() - inboxPage.filterInbox("Unread") - inboxPage.swipeConversationRight(conversation) - inboxPage.assertConversationNotDisplayed(conversation.subject!!) - - inboxPage.swipeConversationLeft(unreadConversation) - inboxPage.assertConversationNotDisplayed(unreadConversation.subject!!) - - inboxPage.filterInbox("Archived") - inboxPage.assertConversationDisplayed(unreadConversation.subject!!) - } - - @Test - @TestMetaData(Priority.NICE_TO_HAVE, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun testInbox_swipeGesturesInSentScope() { - val data = createInitialData() - val sentConversation = data.addConversation( - senderId = data.students.first().id, - receiverIds = listOf(data.teachers.first().id), - messageBody = "Sent body", - messageSubject = "Sent Subject") - val sentConversation2 = data.createBasicConversation(data.students.first().id, messageBody = "Sent Body 2") - data.conversations[sentConversation2.id] = sentConversation2 - - dashboardPage.clickInboxTab() - inboxPage.filterInbox("Sent") - inboxPage.swipeConversationRight(sentConversation) - inboxPage.assertUnreadMarkerVisibility(sentConversation.subject!!, ViewMatchers.Visibility.GONE) - - inboxPage.swipeConversationRight(sentConversation) - inboxPage.assertUnreadMarkerVisibility(sentConversation.subject!!, ViewMatchers.Visibility.VISIBLE) - - inboxPage.filterInbox("Unread") - inboxPage.assertConversationDisplayed(sentConversation.subject!!) - } - - @Test - @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun testInbox_swipeToArchive() { - val data = createInitialData() - val conversation = data.addConversation( - senderId = data.teachers.first().id, - receiverIds = listOf(data.students.first().id), - messageBody = "Body", - messageSubject = "Subject") - - dashboardPage.clickInboxTab() - inboxPage.swipeConversationLeft(conversation) - inboxPage.assertConversationNotDisplayed(conversation.subject!!) - - inboxPage.filterInbox("Archived") - inboxPage.assertConversationDisplayed(conversation.subject!!) - } - - @Test - @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun testInbox_swipeGesturesInArchivedScope() { - val data = createInitialData() - val conversation = data.addConversation( - senderId = data.teachers.first().id, - receiverIds = listOf(data.students.first().id), - messageBody = "Body", - messageSubject = "Subject") - val archivedConversation = data.createBasicConversation(data.teachers.first().id, subject = "Archived subject", workflowState = Conversation.WorkflowState.ARCHIVED, messageBody = "Body 2") - data.conversations[archivedConversation.id] = archivedConversation - - dashboardPage.clickInboxTab() - inboxPage.swipeConversationLeft(conversation) - - inboxPage.filterInbox("Archived") - inboxPage.assertConversationDisplayed(conversation.subject!!) - inboxPage.swipeConversationRight(conversation) - inboxPage.assertConversationNotDisplayed(conversation.subject!!) //Because an Unread conversation cannot be Archived. - - inboxPage.swipeConversationLeft(archivedConversation) - inboxPage.assertConversationNotDisplayed(archivedConversation.subject!!) - - inboxPage.filterInbox("Inbox") - inboxPage.assertConversationDisplayed(conversation.subject!!) - inboxPage.assertUnreadMarkerVisibility(conversation.subject!!, ViewMatchers.Visibility.VISIBLE) - inboxPage.assertConversationDisplayed(archivedConversation.subject!!) - } - - @Test - @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun testInbox_swipeToUnstar() { - val data = createInitialData() - val conversation = data.addConversation( - senderId = data.teachers.first().id, - receiverIds = listOf(data.students.first().id), - messageBody = "Body", - messageSubject = "Subject") - data.addConversations(userId = student1.id, messageBody = "Short body") - data.conversations[conversation.id] = conversation.copy(isStarred = true) - - dashboardPage.clickInboxTab() - inboxPage.filterInbox("Starred") - inboxPage.assertUnreadMarkerVisibility(conversation.subject!!, ViewMatchers.Visibility.VISIBLE) - inboxPage.swipeConversationRight(conversation) - inboxPage.assertUnreadMarkerVisibility(conversation.subject!!, ViewMatchers.Visibility.GONE) - inboxPage.swipeConversationRight(conversation) - inboxPage.assertUnreadMarkerVisibility(conversation.subject!!, ViewMatchers.Visibility.VISIBLE) - - inboxPage.swipeConversationLeft(conversation) - inboxPage.assertConversationNotDisplayed(conversation.subject!!) - } - private fun getFirstConversation(data: MockCanvas, includeIsAuthor: Boolean = false): Conversation { return data.conversations.values.toList() .filter { it.workflowState != Conversation.WorkflowState.ARCHIVED } diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxListInteractionsTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxListInteractionsTest.kt new file mode 100644 index 0000000000..33f3ca7445 --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentInboxListInteractionsTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.instructure.student.ui.interaction + +import com.instructure.canvas.espresso.common.interaction.InboxListInteractionTest +import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockCanvas.addRecipientsToCourse +import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvasapi2.models.CanvasContextPermission +import com.instructure.canvasapi2.models.User +import com.instructure.student.BuildConfig +import com.instructure.student.activity.LoginActivity +import com.instructure.student.ui.pages.DashboardPage +import com.instructure.student.ui.utils.StudentActivityTestRule +import com.instructure.student.ui.utils.tokenLogin +import dagger.hilt.android.testing.HiltAndroidTest + +@HiltAndroidTest +class StudentInboxListInteractionsTest : InboxListInteractionTest() { + + override val isTesting = BuildConfig.IS_TESTING + override val activityRule = StudentActivityTestRule(LoginActivity::class.java) + + private val dashboardPage = DashboardPage() + + override fun goToInbox(data: MockCanvas) { + val student = data.students[0] + val token = data.tokenFor(student)!! + tokenLogin(data.domain, token, student) + + dashboardPage.clickInboxTab() + } + + override fun createInitialData(courseCount: Int): MockCanvas { + val data = MockCanvas.init( + studentCount = 1, + courseCount = courseCount, + teacherCount = 1, + favoriteCourseCount = courseCount + ) + + val course1 = data.courses.values.first() + + data.addCoursePermissions( + course1.id, + CanvasContextPermission(send_messages_all = true, send_messages = true) + ) + + data.addRecipientsToCourse( + course = course1, + students = data.students, + teachers = data.teachers + ) + + return data + } + + override fun getLoggedInUser(): User { + return MockCanvas.data.students[0] + } + + override fun getOtherUser(): User { + return MockCanvas.data.teachers[0] + } +} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt index 4ede8e10ec..bcd4fadcbe 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/DashboardPage.kt @@ -426,6 +426,11 @@ class DashboardPage : BasePage(R.id.dashboardPage) { onView(withId(R.id.bottomNavigationNotifications)).check(matches(isNotEnabled())) onView(withId(R.id.bottomNavigationInbox)).check(matches(isNotEnabled())) } + + fun goToDashboard() { + onView(withId(R.id.bottomNavigationHome)).click() + } + } /** diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt index 606f990da9..5ef1bd7f9d 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/utils/StudentTest.kt @@ -65,7 +65,7 @@ import com.instructure.student.ui.pages.HelpPage import com.instructure.student.ui.pages.HomeroomPage import com.instructure.student.ui.pages.ImportantDatesPage import com.instructure.student.ui.pages.InboxConversationPage -import com.instructure.student.ui.pages.InboxPage +import com.instructure.canvas.espresso.common.pages.InboxPage import com.instructure.student.ui.pages.LeftSideNavigationDrawerPage import com.instructure.student.ui.pages.LegalPage import com.instructure.student.ui.pages.LoginFindSchoolPage diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AddMessagePageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AddMessagePageTest.kt index 7d2a0ae9c7..7c15d73833 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AddMessagePageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/AddMessagePageTest.kt @@ -47,7 +47,7 @@ class AddMessagePageTest: TeacherTest() { fun displayPageObjectsNewMessage() { logIn() dashboardPage.clickInboxTab() - inboxPage.clickAddMessageFAB() + inboxPage.pressNewMessageButton() addMessagePage.assertComposeNewMessageObjectsDisplayed() } @@ -67,7 +67,7 @@ class AddMessagePageTest: TeacherTest() { dashboardPage.clickInboxTab() val chosenConversation = data.conversations.values.first { item -> item.isStarred } - inboxPage.clickConversation(chosenConversation) + inboxPage.openConversation(chosenConversation) inboxMessagePage.clickReply() } } diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/ChooseRecipientsPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/ChooseRecipientsPageTest.kt index a7b628e2a5..7b9d2b3f83 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/ChooseRecipientsPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/ChooseRecipientsPageTest.kt @@ -74,7 +74,7 @@ class ChooseRecipientsPageTest: TeacherTest() { tokenLogin(data.domain, token, teacher) dashboardPage.clickInboxTab() - inboxPage.clickAddMessageFAB() + inboxPage.pressNewMessageButton() addMessagePage.clickCourseSpinner() addMessagePage.selectCourseFromSpinner(course) // Sigh... Sometimes, at least on my local machine, it takes a beat for this button to become responsive diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InboxMessagePageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InboxMessagePageTest.kt index cb1eb6ce22..6c90f86580 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InboxMessagePageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InboxMessagePageTest.kt @@ -54,6 +54,6 @@ class InboxMessagePageTest: TeacherTest() { val token = data.tokenFor(teacher)!! tokenLogin(data.domain, token, teacher) dashboardPage.clickInboxTab() - inboxPage.clickConversation(chosenConversation) + inboxPage.openConversation(chosenConversation) } } diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherInboxListPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherInboxListPageTest.kt new file mode 100644 index 0000000000..bd709973b1 --- /dev/null +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherInboxListPageTest.kt @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.instructure.teacher.ui + +import com.instructure.canvas.espresso.common.interaction.InboxListInteractionTest +import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockCanvas.addCoursePermissions +import com.instructure.canvas.espresso.mockCanvas.addRecipientsToCourse +import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvasapi2.models.CanvasContextPermission +import com.instructure.canvasapi2.models.User +import com.instructure.teacher.BuildConfig +import com.instructure.teacher.activities.LoginActivity +import com.instructure.teacher.ui.pages.DashboardPage +import com.instructure.teacher.ui.utils.TeacherActivityTestRule +import com.instructure.teacher.ui.utils.clickInboxTab +import com.instructure.teacher.ui.utils.tokenLogin +import dagger.hilt.android.testing.HiltAndroidTest + +@HiltAndroidTest +class TeacherInboxListPageTest : InboxListInteractionTest() { + + override val isTesting = BuildConfig.IS_TESTING + override val activityRule = TeacherActivityTestRule(LoginActivity::class.java) + + private val dashboardPage = DashboardPage() + + override fun goToInbox(data: MockCanvas) { + val teacher = data.teachers[0] + val token = data.tokenFor(teacher)!! + tokenLogin(data.domain, token, teacher) + + dashboardPage.clickInboxTab() + } + + override fun createInitialData(courseCount: Int): MockCanvas { + val data = MockCanvas.init( + studentCount = 1, + courseCount = courseCount, + teacherCount = 1, + favoriteCourseCount = courseCount + ) + + val course1 = data.courses.values.first() + + data.addCoursePermissions( + course1.id, + CanvasContextPermission(send_messages_all = true, send_messages = true) + ) + + data.addRecipientsToCourse( + course = course1, + students = data.students, + teachers = data.teachers + ) + + return data + } + + override fun getLoggedInUser(): User { + return MockCanvas.data.teachers[0] + } + + override fun getOtherUser(): User { + return MockCanvas.data.students[0] + } +} \ No newline at end of file diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt index 2ae6c04d5f..f33aa63a65 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/InboxE2ETest.kt @@ -61,13 +61,13 @@ class InboxE2ETest : TeacherTest() { ) Log.d(STEP_TAG,"Refresh the page. Assert that the previously seeded Inbox conversation is displayed. Assert that the message is unread yet.") - inboxPage.refresh() + inboxPage.refreshInbox() inboxPage.assertHasConversation() inboxPage.assertThereIsAnUnreadMessage(true) val replyMessage = "Hello there" Log.d(STEP_TAG,"Click on the conversation. Write a reply with the message: '$replyMessage'.") - inboxPage.clickConversation(seedConversation[0]) + inboxPage.openConversation(seedConversation[0].subject) inboxMessagePage.clickReply() addMessagePage.addReply(replyMessage) @@ -79,7 +79,7 @@ class InboxE2ETest : TeacherTest() { inboxPage.assertThereIsAnUnreadMessage(false) Log.d(STEP_TAG,"Add a new conversation message manually via UI. Click on 'New Message' ('+') button.") - inboxPage.clickAddMessageFAB() + inboxPage.pressNewMessageButton() Log.d(STEP_TAG,"Select '${course.name}' from course spinner. Click on the '+' icon next to the recipients input field. Select the two students: '${student1.name}' and '${student2.name}'. Click on 'Done'.") addNewMessage(course,data.studentsList) @@ -90,13 +90,13 @@ class InboxE2ETest : TeacherTest() { addMessagePage.clickSendButton() Log.d(STEP_TAG,"Filter the Inbox by selecting 'Sent' category from the spinner on Inbox Page.") - inboxPage.filterMessageScope("Sent") + inboxPage.filterInbox("Sent") Log.d(STEP_TAG,"Assert that the previously sent conversation is displayed.") inboxPage.assertHasConversation() Log.d(STEP_TAG,"Click on '$subject' conversation.") - inboxPage.clickConversation(subject) + inboxPage.openConversation(subject) val replyMessageTwo = "Test Reply 2" Log.d(STEP_TAG,"Click on 'Reply' button. Write a reply with the message: '$replyMessageTwo'.") @@ -111,14 +111,14 @@ class InboxE2ETest : TeacherTest() { inboxPage.assertHasConversation() Log.d(STEP_TAG,"Filter the Inbox by selecting 'Inbox' category from the spinner on Inbox Page.") - inboxPage.filterMessageScope("Inbox") + inboxPage.filterInbox("Inbox") Log.d(STEP_TAG,"Refresh the page. Assert that the previously seeded Inbox conversation is displayed.") - inboxPage.refresh() + inboxPage.refreshInbox() inboxPage.assertHasConversation() Log.d(STEP_TAG,"Click on the conversation. Write a reply with the message: '$replyMessage'.") - inboxPage.clickConversation(seedConversation[0]) + inboxPage.openConversation(seedConversation[0].subject) Log.d(STEP_TAG, "Star the conversation and navigate back to Inbox Page.") inboxMessagePage.clickOnStarConversation() @@ -128,7 +128,7 @@ class InboxE2ETest : TeacherTest() { inboxPage.assertConversationStarred(seedConversation[0].subject) Log.d(STEP_TAG,"Click on the conversation. Write a reply with the message: '$replyMessage'.") - inboxPage.clickConversation(seedConversation[0]) + inboxPage.openConversation(seedConversation[0].subject) Log.d(STEP_TAG, "Archive the '${seedConversation[0]}' conversation and assert that it has disappeared from the list," + "because archived conversations does not displayed within the 'All' section.") @@ -138,16 +138,16 @@ class InboxE2ETest : TeacherTest() { Log.d(STEP_TAG,"Filter the Inbox by selecting 'Archived' category from the spinner on Inbox Page." + "Assert that the previously archived conversation is displayed.") - inboxPage.filterMessageScope("Archived") + inboxPage.filterInbox("Archived") inboxPage.assertHasConversation() Log.d(STEP_TAG,"Filter the Inbox by selecting 'Starred' category from the spinner on Inbox Page." + "Assert that the '${seedConversation[0]}' conversation is displayed because it's still starred.") - inboxPage.filterMessageScope("Starred") + inboxPage.filterInbox("Starred") inboxPage.assertHasConversation() Log.d(STEP_TAG,"Click on the conversation.") - inboxPage.clickConversation(seedConversation[0]) + inboxPage.openConversation(seedConversation[0].subject) Log.d(STEP_TAG, "Remove star from the conversation and navigate back to Inbox Page.") inboxMessagePage.clickOnStarConversation() @@ -188,7 +188,7 @@ class InboxE2ETest : TeacherTest() { val seedConversation = ConversationsApi.createConversation(token = student1.token, recipients = listOf(teacher.id.toString())) Log.d(STEP_TAG, "Refresh the page. Assert that the conversation displayed as unread.") - inboxPage.refresh() + inboxPage.refreshInbox() inboxPage.assertThereIsAnUnreadMessage(true) Log.d(PREPARATION_TAG, "Seed another Inbox conversation via API.") @@ -198,17 +198,17 @@ class InboxE2ETest : TeacherTest() { val seedConversation3 = ConversationsApi.createConversation(token = student2.token, recipients = listOf(teacher.id.toString()), subject = "Third conversation", body = "Third body") Log.d(STEP_TAG,"Refresh the page. Filter the Inbox by selecting 'Inbox' category from the spinner on Inbox Page. Assert that the '${seedConversation[0]}' conversation is displayed. Assert that the conversation is unread yet.") - inboxPage.refresh() - inboxPage.filterMessageScope("Inbox") + inboxPage.refreshInbox() + inboxPage.filterInbox("Inbox") inboxPage.assertHasConversation() Log.d(STEP_TAG, "Select '${seedConversation2[0].subject}' conversation. Unarchive it, and assert that it is not displayed in the 'ARCHIVED' scope any more.") - inboxPage.selectConversation(seedConversation2[0]) + inboxPage.selectConversation(seedConversation2[0].subject) inboxPage.clickArchive() inboxPage.assertConversationNotDisplayed(seedConversation2[0].subject) Log.d(STEP_TAG, "Select 'ARCHIVED' scope and assert that '${seedConversation2[0].subject}' conversation is displayed in the 'ARCHIVED' scope.") - inboxPage.filterMessageScope("Archived") + inboxPage.filterInbox("Archived") retry(times = 10, delay = 3000, block = { refresh() @@ -217,13 +217,13 @@ class InboxE2ETest : TeacherTest() { Log.d(STEP_TAG, "Select '${seedConversation2[0].subject}' conversation and unarchive it." + "Assert that the selected number of conversation on the toolbar is 1 and '${seedConversation2[0].subject}' conversation is not displayed in the 'ARCHIVED' scope.") - inboxPage.selectConversation(seedConversation2[0]) + inboxPage.selectConversation(seedConversation2[0].subject) inboxPage.assertSelectedConversationNumber("1") inboxPage.clickUnArchive() inboxPage.assertConversationNotDisplayed(seedConversation2[0].subject) Log.d(STEP_TAG,"Navigate to 'INBOX' scope and assert that '${seedConversation2[0].subject}' conversation is displayed.") - inboxPage.filterMessageScope("Inbox") + inboxPage.filterInbox("Inbox") inboxPage.assertConversationDisplayed(seedConversation2[0].subject) Log.d(STEP_TAG, "Select both of the conversations '${seedConversation[0].subject}' and '${seedConversation2[0].subject}' and star them." + @@ -250,17 +250,17 @@ class InboxE2ETest : TeacherTest() { inboxPage.assertConversationNotDisplayed(seedConversation3[0].subject) Log.d(STEP_TAG, "Navigate to 'ARCHIVED' scope and assert that both of the conversations are displayed there.") - inboxPage.filterMessageScope("Archived") + inboxPage.filterInbox("Archived") inboxPage.assertConversationDisplayed(seedConversation2[0].subject) inboxPage.assertConversationDisplayed(seedConversation3[0].subject) Log.d(STEP_TAG, "Navigate to 'UNREAD' scope and assert that none of the conversations are displayed there, because a conversation cannot be archived and unread at the same time.") - inboxPage.filterMessageScope("Unread") + inboxPage.filterInbox("Unread") inboxPage.assertConversationNotDisplayed(seedConversation2[0].subject) inboxPage.assertConversationNotDisplayed(seedConversation3[0].subject) Log.d(STEP_TAG, "Navigate to 'STARRED' scope and assert that both of the conversations are displayed there.") - inboxPage.filterMessageScope("Starred") + inboxPage.filterInbox("Starred") inboxPage.assertConversationDisplayed(seedConversation2[0].subject) inboxPage.assertConversationDisplayed(seedConversation3[0].subject) @@ -271,7 +271,7 @@ class InboxE2ETest : TeacherTest() { inboxPage.assertConversationNotDisplayed(seedConversation3[0].subject) Log.d(STEP_TAG, "Navigate to 'ARCHIVED' scope and assert that both of the conversations are displayed there.") - inboxPage.filterMessageScope("Archived") + inboxPage.filterInbox("Archived") inboxPage.assertConversationDisplayed(seedConversation2[0].subject) inboxPage.assertConversationDisplayed(seedConversation3[0].subject) @@ -282,7 +282,7 @@ class InboxE2ETest : TeacherTest() { inboxPage.assertConversationNotDisplayed(seedConversation3[0].subject) Log.d(STEP_TAG, "Navigate to 'INBOX' scope and assert that both of the conversations are displayed there.") - inboxPage.filterMessageScope("Inbox") + inboxPage.filterInbox("Inbox") inboxPage.assertConversationDisplayed(seedConversation2[0].subject) inboxPage.assertConversationDisplayed(seedConversation3[0].subject) } @@ -317,7 +317,7 @@ class InboxE2ETest : TeacherTest() { ConversationsApi.createConversation(token = student1.token, recipients = listOf(teacher.id.toString())) Log.d(STEP_TAG,"Refresh the page. Assert that the previously seeded Inbox conversation is displayed. Assert that the message is unread yet.") - inboxPage.refresh() + inboxPage.refreshInbox() inboxPage.assertHasConversation() inboxPage.assertThereIsAnUnreadMessage(true) @@ -338,29 +338,29 @@ class InboxE2ETest : TeacherTest() { ) Log.d(STEP_TAG, "Select '${seedConversation2[0].subject}' conversation and swipe it right to make it read. Assert that the conversation became read.") - inboxPage.refresh() + inboxPage.refreshInbox() inboxPage.selectConversation(seedConversation2[0].subject) - inboxPage.swipeConversationRight(seedConversation2[0]) + inboxPage.swipeConversationRight(seedConversation2[0].subject) inboxPage.assertUnreadMarkerVisibility(seedConversation2[0].subject, ViewMatchers.Visibility.GONE) Log.d(STEP_TAG, "Select '${seedConversation2[0].subject}' conversation and swipe it right again to make it unread. Assert that the conversation became unread.") - inboxPage.swipeConversationRight(seedConversation2[0]) + inboxPage.swipeConversationRight(seedConversation2[0].subject) inboxPage.assertUnreadMarkerVisibility(seedConversation2[0].subject, ViewMatchers.Visibility.VISIBLE) Log.d(STEP_TAG, "Swipe '${seedConversation2[0].subject}' left and assert it is removed from the 'INBOX' scope because it has became archived.") - inboxPage.swipeConversationLeft(seedConversation2[0]) + inboxPage.swipeConversationLeft(seedConversation2[0].subject) inboxPage.assertConversationNotDisplayed(seedConversation2[0].subject) Log.d(STEP_TAG, "Navigate to 'ARCHIVED' scope. Assert that the '${seedConversation2[0].subject}' conversation is displayed in the 'ARCHIVED' scope.") - inboxPage.filterMessageScope("Archived") + inboxPage.filterInbox("Archived") inboxPage.assertConversationDisplayed(seedConversation2[0].subject) Log.d(STEP_TAG, "Swipe '${seedConversation2[0].subject}' left and assert it is removed from the 'ARCHIVED' scope because it has became unarchived.") - inboxPage.swipeConversationLeft(seedConversation2[0]) + inboxPage.swipeConversationLeft(seedConversation2[0].subject) inboxPage.assertConversationNotDisplayed(seedConversation2[0].subject) Log.d(STEP_TAG, "Navigate to 'INBOX' scope. Assert that the '${seedConversation2[0].subject}' conversation is displayed in the 'INBOX' scope.") - inboxPage.filterMessageScope("Inbox") + inboxPage.filterInbox("Inbox") inboxPage.assertConversationDisplayed(seedConversation2[0].subject) Log.d(STEP_TAG, "Select both of the conversations. Star them and mark the unread.") @@ -376,7 +376,7 @@ class InboxE2ETest : TeacherTest() { inboxPage.clickStar() Log.d(STEP_TAG, "Navigate to 'STARRED' scope. Assert that both of the conversation are displayed in the 'STARRED' scope.") - inboxPage.filterMessageScope("Starred") + inboxPage.filterInbox("Starred") retryWithIncreasingDelay(times = 10, maxDelay = 3000, catchBlock = { refresh() }) { inboxPage.assertConversationDisplayed(seedConversation2[0].subject) @@ -384,7 +384,7 @@ class InboxE2ETest : TeacherTest() { } Log.d(STEP_TAG, "Swipe '${seedConversation2[0].subject}' left and assert it is removed from the 'STARRED' scope because it has became unstarred.") - inboxPage.swipeConversationLeft(seedConversation2[0]) + inboxPage.swipeConversationLeft(seedConversation2[0].subject) inboxPage.assertConversationNotDisplayed(seedConversation2[0].subject) Log.d(STEP_TAG, "Swipe '${seedConversation3[0].subject}' conversation right and assert that it has became unread.") @@ -392,20 +392,20 @@ class InboxE2ETest : TeacherTest() { inboxPage.assertUnreadMarkerVisibility(seedConversation3[0].subject, ViewMatchers.Visibility.VISIBLE) Log.d(STEP_TAG, "Navigate to 'UNREAD' scope. Assert that only the '${seedConversation3[0].subject}' conversation is displayed in the 'UNREAD' scope.") - inboxPage.filterMessageScope("Unread") + inboxPage.filterInbox("Unread") inboxPage.assertConversationDisplayed(seedConversation3[0].subject) inboxPage.assertConversationNotDisplayed(seedConversation2[0].subject) Log.d(STEP_TAG, "Swipe '${seedConversation3[0].subject}' conversation left and assert it has been removed from the 'UNREAD' scope since it has became read.") - inboxPage.swipeConversationLeft(seedConversation3[0]) + inboxPage.swipeConversationLeft(seedConversation3[0].subject) inboxPage.assertConversationNotDisplayed(seedConversation3[0].subject) Log.d(STEP_TAG, "Navigate to 'ARCHIVED' scope. Assert that the '${seedConversation2[0].subject}' conversation is displayed in the 'ARCHIVED' scope.") - inboxPage.filterMessageScope("Archived") + inboxPage.filterInbox("Archived") inboxPage.assertConversationDisplayed(seedConversation3[0].subject) Log.d(STEP_TAG, "Navigate to 'INBOX' scope and select '${seedConversation3[0].subject}' conversation.") - inboxPage.filterMessageScope("Inbox") + inboxPage.filterInbox("Inbox") inboxPage.selectConversation(seedConversation2[0].subject) Log.d(STEP_TAG, "Delete the '${seedConversation2[0].subject}' conversation and assert that it has been removed from the 'INBOX' scope.") @@ -414,10 +414,10 @@ class InboxE2ETest : TeacherTest() { inboxPage.assertConversationNotDisplayed(seedConversation2[0].subject) Log.d(STEP_TAG, "Navigate to 'ARCHIVED' scope. Assert that the '${seedConversation3[0].subject}' conversation is displayed in the 'ARCHIVED' scope.") - inboxPage.filterMessageScope("Archived") + inboxPage.filterInbox("Archived") Log.d(STEP_TAG,"Click on the '${seedConversation3[0].subject}' conversation.") - inboxPage.clickConversation(seedConversation3[0]) + inboxPage.openConversation(seedConversation3[0].subject) Log.d(STEP_TAG, "Delete the '${seedConversation3[0]}' conversation and assert that it has disappeared from the list.") inboxMessagePage.deleteConversation() @@ -467,7 +467,7 @@ class InboxE2ETest : TeacherTest() { ) Log.d(STEP_TAG,"Refresh the page. Assert that the previously seeded Inbox conversation is displayed. Assert that the message is unread yet.") - inboxPage.refresh() + inboxPage.refreshInbox() Log.d(STEP_TAG, "Assert that the '${seedConversation2[0].subject}' conversation, which was sent by the Student to the Teacher is displayed in the 'Inbox' filter view," + "and the '${seedConversation[0].subject}' conversation, which was sent by the Teacher to the Student is not displayed in the 'Inbox' filter view.") @@ -476,13 +476,13 @@ class InboxE2ETest : TeacherTest() { Log.d(STEP_TAG, "Filter to 'Sent' messages. Assert that the '${seedConversation[0].subject}' conversation, which was sent by the Teacher to the Student is displayed in the 'Inbox' filter view," + "and the '${seedConversation2[0].subject}' conversation, which was sent by the Student to the Teacher is not displayed in the 'Inbox' filter view.") - inboxPage.filterMessageScope("Sent") + inboxPage.filterInbox("Sent") inboxPage.assertConversationDisplayed(seedConversation[0].subject) inboxPage.assertConversationNotDisplayed(seedConversation2[0].subject) Log.d(STEP_TAG, "Filter to '${course.name}' course messages and filter to 'Inbox' messages.") - inboxPage.filterCourseScope(course.name) - inboxPage.filterMessageScope("Inbox") + inboxPage.selectInboxFilter(course.name) + inboxPage.filterInbox("Inbox") Log.d(STEP_TAG, "Assert that the '${seedConversation2[0].subject}' conversation, which was sent by the Student to the Teacher is displayed in the 'Inbox' filter view," + "and the '${seedConversation[0].subject}' conversation, which was sent by the Teacher to the Student is not displayed in the 'Inbox' filter view.") @@ -491,18 +491,18 @@ class InboxE2ETest : TeacherTest() { Log.d(STEP_TAG, "Filter to 'Sent' messages. Assert that the '${seedConversation[0].subject}' conversation, which was sent by the Teacher to the Student is displayed in the 'Inbox' filter view," + "and the '${seedConversation2[0].subject}' conversation, which was sent by the Student to the Teacher is not displayed in the 'Inbox' filter view.") - inboxPage.filterMessageScope("Sent") + inboxPage.filterInbox("Sent") inboxPage.assertConversationDisplayed(seedConversation[0].subject) inboxPage.assertConversationNotDisplayed(seedConversation2[0].subject) Log.d(STEP_TAG, "Filter to '${course.name}' course messages. Assert that there is no message in the '${course.name}' course's 'Inbox' filter view," + "because the seeded conversations does not belong to this course.") - inboxPage.filterCourseScope(course2.name) + inboxPage.selectInboxFilter(course2.name) inboxPage.assertInboxEmpty() Log.d(STEP_TAG, "Filter to 'Sent' messages among the '${course.name}' course's messages. Assert that there is no message in the '${course.name}' course's 'Sent' filter view," + "because the seeded conversations does not belong to this course.") - inboxPage.filterMessageScope("Sent") + inboxPage.filterInbox("Sent") inboxPage.assertInboxEmpty() Log.d(STEP_TAG, "Clear course filter (so get back to 'All Courses' view." + @@ -514,7 +514,7 @@ class InboxE2ETest : TeacherTest() { Log.d(STEP_TAG, "Filter to 'Inbox' messages. Assert that the '${seedConversation2[0].subject}' conversation, which was sent by the Teacher to the Student is displayed in the 'Inbox' filter view," + "and the '${seedConversation[0].subject}' conversation, which was sent by the Student to the Teacher is not displayed in the 'Inbox' filter view.") - inboxPage.filterMessageScope("Inbox") + inboxPage.filterInbox("Inbox") inboxPage.assertConversationDisplayed(seedConversation2[0].subject) inboxPage.assertConversationNotDisplayed(seedConversation[0].subject) } diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PeopleE2ETest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PeopleE2ETest.kt index f5b64c3b61..892e53c9e2 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PeopleE2ETest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/e2e/PeopleE2ETest.kt @@ -208,7 +208,7 @@ class PeopleE2ETest: TeacherTest() { inboxPage.assertInboxEmpty() Log.d(STEP_TAG,"Filter the Inbox by selecting 'Sent' category from the spinner on Inbox Page.") - inboxPage.filterMessageScope("Sent") + inboxPage.filterInbox("Sent") Log.d(STEP_TAG,"Assert that the previously sent conversation is displayed.") inboxPage.assertHasConversation() diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/InboxPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/InboxPage.kt deleted file mode 100644 index 51a767489a..0000000000 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/InboxPage.kt +++ /dev/null @@ -1,443 +0,0 @@ -package com.instructure.teacher.ui.pages - -import androidx.test.espresso.Espresso -import androidx.test.espresso.action.ViewActions -import androidx.test.espresso.assertion.ViewAssertions -import androidx.test.espresso.matcher.ViewMatchers -import androidx.test.espresso.matcher.ViewMatchers.hasSibling -import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast -import androidx.test.platform.app.InstrumentationRegistry -import com.instructure.canvas.espresso.scrollRecyclerView -import com.instructure.canvas.espresso.waitForMatcherWithRefreshes -import com.instructure.canvas.espresso.withCustomConstraints -import com.instructure.canvasapi2.models.Conversation -import com.instructure.dataseeding.model.ConversationApiModel -import com.instructure.espresso.OnViewWithId -import com.instructure.espresso.RecyclerViewItemCountAssertion -import com.instructure.espresso.RecyclerViewItemCountGreaterThanAssertion -import com.instructure.espresso.WaitForViewWithId -import com.instructure.espresso.assertDisplayed -import com.instructure.espresso.assertVisibility -import com.instructure.espresso.click -import com.instructure.espresso.longClick -import com.instructure.espresso.page.BasePage -import com.instructure.espresso.page.onView -import com.instructure.espresso.page.plus -import com.instructure.espresso.page.waitForView -import com.instructure.espresso.page.waitForViewWithId -import com.instructure.espresso.page.waitForViewWithText -import com.instructure.espresso.page.withAncestor -import com.instructure.espresso.page.withId -import com.instructure.espresso.page.withText -import com.instructure.espresso.scrollTo -import com.instructure.espresso.swipeLeft -import com.instructure.espresso.swipeRight -import com.instructure.teacher.R -import com.instructure.teacher.ui.utils.WaitForToolbarTitle -import org.hamcrest.Matchers - -/** - * Represents the Inbox Page. - * - * This page extends the BasePage class and provides functionality for interacting with the inbox. - * It contains various view elements such as toolbar, inbox recycler view, add message FAB, - * empty inbox view, scope filter text, and edit toolbar. - */ -class InboxPage: BasePage() { - - private val toolbarTitle by WaitForToolbarTitle(R.string.tab_inbox) - - private val inboxRecyclerView by WaitForViewWithId(R.id.inboxRecyclerView) - - private val addMessageFAB by WaitForViewWithId(R.id.addMessage) - - //Only displayed when inbox is empty - private val emptyPandaView by WaitForViewWithId(R.id.emptyInboxView) - private val scopeFilterText by OnViewWithId(R.id.scopeFilterText) - private val editToolbar by OnViewWithId(R.id.editToolbar) - - override fun assertPageObjects(duration: Long) { - toolbarTitle.assertDisplayed() - } - - /** - * Asserts that the inbox has at least one conversation. - */ - fun assertHasConversation() { - assertConversationCountIsGreaterThan(0) - } - - /** - * Asserts that the count of conversations is greater than the specified count. - * - * @param count The count to compare against. - */ - fun assertConversationCountIsGreaterThan(count: Int) { - inboxRecyclerView.check(RecyclerViewItemCountGreaterThanAssertion(count)) - } - - /** - * Asserts that the count of conversations matches the specified count. - * - * @param count The expected count of conversations. - */ - fun assertConversationCount(count: Int) { - inboxRecyclerView.check(RecyclerViewItemCountAssertion(count)) - } - - /** - * Clicks on the conversation with the specified subject. - * - * @param conversationSubject The subject of the conversation to click. - */ - fun clickConversation(conversationSubject: String) { - waitForViewWithText(conversationSubject).click() - } - - /** - * Clicks on the conversation with the specified subject. - * - * @param conversation The subject of the conversation to click. - */ - fun clickConversation(conversation: ConversationApiModel) { - clickConversation(conversation.subject) - } - - /** - * Clicks on the conversation with the specified subject. - * - * @param conversation The subject of the conversation to click. - */ - fun clickConversation(conversation: Conversation) { - clickConversation(conversation.subject!!) - } - - /** - * Clicks on the add message FAB. - */ - fun clickAddMessageFAB() { - addMessageFAB.click() - } - - /** - * Asserts that the inbox is empty. - */ - fun assertInboxEmpty() { - onView(withId(R.id.emptyInboxView)).assertDisplayed() - } - - /** - * Refreshes the inbox view. - */ - fun refresh() { - onView(withId(R.id.swipeRefreshLayout)) - .perform(withCustomConstraints(ViewActions.swipeDown(), isDisplayingAtLeast(50))) - } - - /** - * Filters the messages by the specified filter. - * - * @param filterFor The filter to apply. - */ - fun filterMessageScope(filterFor: String) { - waitForView(withId(R.id.scopeFilterText)) - onView(withId(R.id.scopeFilter)).click() - waitForViewWithText(filterFor).click() - } - - /** - * Filters the messages by the specified course scope. - * - * @param courseName The name of the course to filter for. - */ - fun filterCourseScope(courseName: String) { - waitForView(withId(R.id.courseFilter)).click() - waitForViewWithText(courseName).click() - } - - /** - * Clears the course filter. - */ - fun clearCourseFilter() { - waitForView(withId(R.id.courseFilter)).click() - onView(withId(R.id.clear) + withText(R.string.inboxClearFilter)).click() - } - - /** - * Asserts whether there is an unread message based on the specified flag. - * - * @param unread Flag indicating whether there is an unread message. - */ - fun assertThereIsAnUnreadMessage(unread: Boolean) { - if(unread) onView(withId(R.id.unreadMark)).assertDisplayed() - else onView(withId(R.id.unreadMark) + ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.GONE)) - } - - /** - * Asserts that the conversation with the specified subject is starred. - * - * @param subject The subject of the conversation. - */ - fun assertConversationStarred(subject: String) { - val matcher = Matchers.allOf( - withId(R.id.star), - ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE), - hasSibling(withId(R.id.userName)), - hasSibling(withId(R.id.date)), - hasSibling(Matchers.allOf(withId(R.id.subjectView), withText(subject))) - ) - waitForMatcherWithRefreshes(matcher) // May need to refresh before the star shows up - onView(matcher).assertDisplayed() - } - - /** - * Asserts that the conversation with the specified subject is not starred. - * - * @param subject The subject of the conversation. - */ - fun assertConversationNotStarred(subject: String) { - val matcher = Matchers.allOf( - withId(R.id.star), - ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE), - hasSibling(withId(R.id.userName)), - hasSibling(withId(R.id.date)), - hasSibling(Matchers.allOf(withId(R.id.subjectView), withText(subject))) - ) - waitForMatcherWithRefreshes(matcher) // May need to refresh before the star shows up - onView(matcher).check(ViewAssertions.doesNotExist()) - } - - /** - * Asserts that the conversation with the specified subject is displayed. - * - * @param subject The subject of the conversation. - */ - fun assertConversationDisplayed(subject: String) { - val matcher = withText(subject) - waitForView(matcher).scrollTo().assertDisplayed() - } - - /** - * Asserts that the conversation with the specified subject is not displayed. - * - * @param subject The subject of the conversation. - */ - fun assertConversationNotDisplayed(subject: String) { - val matcher = withText(subject) - onView(matcher).check(ViewAssertions.doesNotExist()) - } - - /** - * Asserts the visibility of the unread marker for the conversation with the specified subject. - * - * @param subject The subject of the conversation. - * @param visibility The expected visibility of the unread marker. - */ - fun assertUnreadMarkerVisibility(subject: String, visibility: ViewMatchers.Visibility) { - val matcher = Matchers.allOf( - withId(R.id.unreadMark), - ViewMatchers.withEffectiveVisibility(visibility), - hasSibling(Matchers.allOf(withId(R.id.avatar))), - hasSibling(Matchers.allOf(withId(R.id.subjectView), withText(subject))) - ) - if(visibility == ViewMatchers.Visibility.VISIBLE) { - waitForMatcherWithRefreshes(matcher) // May need to refresh before the unread mark shows up - scrollRecyclerView(R.id.inboxRecyclerView, matcher) - onView(matcher).assertDisplayed() - } - else if(visibility == ViewMatchers.Visibility.GONE) { - onView(matcher).check(ViewAssertions.matches(Matchers.not(ViewMatchers.isDisplayed()))) - } - } - - /** - * Selects the conversation with the specified subject. - * - * @param conversationSubject The subject of the conversation to select. - */ - fun selectConversation(conversationSubject: String) { - waitForView(withId(R.id.inboxRecyclerView)) - val matcher = withText(conversationSubject) - onView(matcher).scrollTo().longClick() - } - - /** - * Selects the conversation with the specified subject. - * - * @param conversation The conversation to select. - */ - fun selectConversation(conversation: Conversation) { - selectConversation(conversation.subject!!) - } - - /** - * Selects the conversation with the specified subject. - * - * @param conversation The conversation to select. - */ - fun selectConversation(conversation: ConversationApiModel) { - selectConversation(conversation.subject!!) - } - - /** - * Clicks the archive option in the action mode. - */ - fun clickArchive() { - waitForViewWithId(R.id.inboxArchiveSelected).click() - } - - /** - * Clicks the unarchive option in the action mode. - */ - fun clickUnArchive() { - waitForViewWithId(R.id.inboxUnarchiveSelected).click() - } - - /** - * Clicks the star option in the action mode. - */ - fun clickStar() { - waitForViewWithId(R.id.inboxStarSelected).click() - } - - /** - * Clicks the unstar option in the action mode. - */ - fun clickUnstar() { - waitForViewWithId(R.id.inboxUnstarSelected).click() - } - - /** - * Clicks the mark as read option in the action mode. - */ - fun clickMarkAsRead() { - waitForViewWithId(R.id.inboxMarkAsReadSelected).click() - } - - /** - * Clicks the mark as unread option in the action mode. - */ - fun clickMarkAsUnread() { - waitForViewWithId(R.id.inboxMarkAsUnreadSelected).click() - } - - /** - * Clicks the delete option in the action mode. - */ - fun clickDelete() { - Espresso.openActionBarOverflowOrOptionsMenu( - InstrumentationRegistry.getInstrumentation().getTargetContext() - ) - onView(ViewMatchers.withText("Delete")) - .perform(ViewActions.click()); - } - - /** - * Confirms the delete action. - */ - fun confirmDelete() { - waitForView(withText("DELETE") + withAncestor(R.id.buttonPanel)).click() - } - - /** - * Swipes the conversation with the specified subject to the right. - * - * @param conversationSubject The subject of the conversation to swipe. - */ - fun swipeConversationRight(conversationSubject: String) { - waitForView(withId(R.id.inboxRecyclerView)) - val matcher = withText(conversationSubject) - onView(matcher).scrollTo().swipeRight() - } - - /** - * Swipes the conversation to the right. - * - * @param conversation The conversation to swipe. - */ - fun swipeConversationRight(conversation: ConversationApiModel) { - swipeConversationRight(conversation.subject!!) - } - - /** - * Swipes the conversation to the right. - * - * @param conversation The conversation to swipe. - */ - fun swipeConversationRight(conversation: Conversation) { - swipeConversationRight(conversation.subject!!) - } - - /** - * Swipes the conversation with the specified subject to the left. - * - * @param conversationSubject The subject of the conversation to swipe. - */ - fun swipeConversationLeft(conversationSubject: String) { - waitForView(withId(R.id.inboxRecyclerView)) - val matcher = withText(conversationSubject) - onView(matcher).scrollTo() - onView(matcher).swipeLeft() - } - - /** - * Swipes the conversation to the left. - * - * @param conversation The conversation to swipe. - */ - fun swipeConversationLeft(conversation: Conversation) { - swipeConversationLeft(conversation.subject!!) - } - - /** - * Swipes the conversation to the left. - * - * @param conversation The conversation to swipe. - */ - fun swipeConversationLeft(conversation: ConversationApiModel) { - swipeConversationLeft(conversation.subject!!) - } - - /** - * Selects multiple conversations. - * - * @param conversations The list of conversation subjects to select. - */ - fun selectConversations(conversations: List) { - for(conversation in conversations) { - selectConversation(conversation) - } - } - - /** - * Asserts the selected conversation number in the edit toolbar. - * - * @param selectedConversationNumber The expected selected conversation number. - */ - fun assertSelectedConversationNumber(selectedConversationNumber: String) { - onView(withText(selectedConversationNumber) + withAncestor(R.id.editToolbar)) - } - - /** - * Asserts the visibility of the edit toolbar. - * - * @param visibility The expected visibility of the edit toolbar. - */ - fun assertEditToolbarIs(visibility: ViewMatchers.Visibility) { - editToolbar.assertVisibility(visibility) - } - - /** - * Asserts that the star icon is displayed. - */ - fun assertStarDisplayed() { - waitForViewWithId(R.id.inboxStarSelected).assertDisplayed() - } - - /** - * Asserts that the unstar icon is displayed. - */ - fun assertUnStarDisplayed() { - waitForViewWithId(R.id.inboxUnstarSelected).assertDisplayed() - } -} diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt index 091b5b7583..2f0c4e3d04 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt @@ -24,6 +24,7 @@ import androidx.test.espresso.matcher.ViewMatchers import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.instructure.canvas.espresso.CanvasTest +import com.instructure.canvas.espresso.common.pages.InboxPage import com.instructure.espresso.InstructureActivityTestRule import com.instructure.espresso.ModuleItemInteractions import com.instructure.espresso.Searchable @@ -56,7 +57,6 @@ import com.instructure.teacher.ui.pages.EditSyllabusPage import com.instructure.teacher.ui.pages.FileListPage import com.instructure.teacher.ui.pages.HelpPage import com.instructure.teacher.ui.pages.InboxMessagePage -import com.instructure.teacher.ui.pages.InboxPage import com.instructure.teacher.ui.pages.LeftSideNavigationDrawerPage import com.instructure.teacher.ui.pages.LegalPage import com.instructure.teacher.ui.pages.LoginFindSchoolPage diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InboxPageTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxListInteractionTest.kt similarity index 59% rename from apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InboxPageTest.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxListInteractionTest.kt index 76439889cf..59b8a1855b 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/InboxPageTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/InboxListInteractionTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 - present Instructure, Inc. + * Copyright (C) 2024 - present Instructure, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,68 +12,39 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. - * */ - -package com.instructure.teacher.ui +package com.instructure.canvas.espresso.common.interaction import androidx.test.espresso.matcher.ViewMatchers +import com.instructure.canvas.espresso.CanvasTest import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData +import com.instructure.canvas.espresso.common.pages.InboxPage import com.instructure.canvas.espresso.mockCanvas.MockCanvas import com.instructure.canvas.espresso.mockCanvas.addConversation import com.instructure.canvas.espresso.mockCanvas.addConversations +import com.instructure.canvas.espresso.mockCanvas.addConversationsToCourseMap import com.instructure.canvas.espresso.mockCanvas.createBasicConversation -import com.instructure.canvas.espresso.mockCanvas.init import com.instructure.canvasapi2.models.Conversation import com.instructure.canvasapi2.models.User -import com.instructure.teacher.ui.utils.TeacherTest -import com.instructure.teacher.ui.utils.clickInboxTab -import com.instructure.teacher.ui.utils.tokenLogin -import dagger.hilt.android.testing.HiltAndroidTest import org.junit.Test -@HiltAndroidTest -class InboxPageTest: TeacherTest() { - - @Test - @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - override fun displaysPageObjects() { - val data = createInitialData() - val teacher = data.teachers[0] - data.addConversations(userId = teacher.id) - navigateToInbox(data, teacher) - inboxPage.assertPageObjects() - } +abstract class InboxListInteractionTest : CanvasTest() { + + private val inboxPage = InboxPage() @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun displaysConversation() { - val data = createInitialData() - val teacher = data.teachers[0] - data.addConversations(userId = teacher.id) - - // Test expects single conversation; filter down to starred conversation - val unwanted = data.conversations.filter() {entry -> !entry.value.isStarred} - unwanted.forEach() { (id, conversation) -> data.conversations.remove(id)} - - navigateToInbox(data, teacher) - inboxPage.assertHasConversation() - } - - @Test - @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun showEditToolbarWhenConversationIsSelected() { + fun testInbox_showEditToolbarWhenConversationIsSelected() { val data = createInitialData() val conversation = data.addConversation( - senderId = data.students.first().id, - receiverIds = listOf(data.teachers.first().id), + senderId = getOtherUser().id, + receiverIds = listOf(getLoggedInUser().id), messageBody = "Body", messageSubject = "Subject") - - navigateToInbox(data, data.teachers.first()) + goToInbox(data) inboxPage.selectConversation(conversation) inboxPage.assertEditToolbarIs(ViewMatchers.Visibility.VISIBLE) inboxPage.assertSelectedConversationNumber("1") @@ -81,52 +52,47 @@ class InboxPageTest: TeacherTest() { @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun archiveMultipleConversations() { + fun testInbox_archiveMultipleConversations() { val data = createInitialData() - val isTablet = isTabletDevice() - + data.addConversations(userId = getLoggedInUser().id, messageBody = "Short body") val conversation1 = data.addConversation( - senderId = data.students.first().id, - receiverIds = listOf(data.teachers.first().id), + senderId = getOtherUser().id, + receiverIds = listOf(getLoggedInUser().id), messageBody = "Body", messageSubject = "Subject") val conversation2 = data.addConversation( - senderId = data.students.first().id, - receiverIds = listOf(data.teachers.first().id), + senderId = getOtherUser().id, + receiverIds = listOf(getLoggedInUser().id), messageBody = "Body 2", messageSubject = "Subject 2") - data.addConversations(userId = data.teachers.first().id, messageBody = "Short body") - - navigateToInbox(data, data.teachers.first()) + goToInbox(data) inboxPage.selectConversations(listOf(conversation1.subject!!, conversation2.subject!!)) inboxPage.clickArchive() inboxPage.assertConversationNotDisplayed(conversation1.subject!!) inboxPage.assertConversationNotDisplayed(conversation2.subject!!) inboxPage.assertEditToolbarIs(ViewMatchers.Visibility.GONE) - if(!isTablet) inboxPage.refresh() - inboxPage.filterMessageScope("Archived") + inboxPage.filterInbox("Archived") inboxPage.assertConversationDisplayed(conversation1.subject!!) inboxPage.assertConversationDisplayed(conversation2.subject!!) } @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun starMultipleConversations() { + fun testInbox_starMultipleConversations() { val data = createInitialData() val conversation1 = data.addConversation( - senderId = data.students.first().id, - receiverIds = listOf(data.teachers.first().id), + senderId = getOtherUser().id, + receiverIds = listOf(getLoggedInUser().id), messageBody = "Body", messageSubject = "Subject") val conversation2 = data.addConversation( - senderId = data.students.first().id, - receiverIds = listOf(data.teachers.first().id), + senderId = getOtherUser().id, + receiverIds = listOf(getLoggedInUser().id), messageBody = "Body 2", messageSubject = "Subject 2") - data.addConversations(userId = data.teachers.first().id, messageBody = "Short body") - - navigateToInbox(data, data.teachers.first()) + data.addConversations(userId = getLoggedInUser().id, messageBody = "Short body") + goToInbox(data) inboxPage.selectConversations(listOf(conversation1.subject!!, conversation2.subject!!)) inboxPage.clickStar() inboxPage.assertConversationStarred(conversation1.subject!!) @@ -136,25 +102,26 @@ class InboxPageTest: TeacherTest() { @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun unstarMultipleConversations() { + fun testInbox_unstarMultipleConversations() { val data = createInitialData() - data.addConversations(userId = data.teachers.first().id, messageBody = "Short body") + data.addConversations(userId = getLoggedInUser().id, messageBody = "Short body") val conversation1 = data.addConversation( - senderId = data.students.first().id, - receiverIds = listOf(data.teachers.first().id), + senderId = getOtherUser().id, + receiverIds = listOf(getLoggedInUser().id), messageBody = "Body", messageSubject = "Subject") data.conversations[conversation1.id] = conversation1.copy(isStarred = true) val conversation2 = data.addConversation( - senderId = data.students.first().id, - receiverIds = listOf(data.teachers.first().id), + senderId = getOtherUser().id, + receiverIds = listOf(getLoggedInUser().id), messageBody = "Body 2", messageSubject = "Subject 2") data.conversations[conversation2.id] = conversation2.copy(isStarred = true) - navigateToInbox(data, data.teachers.first()) - inboxPage.filterMessageScope("Starred") + goToInbox(data) + inboxPage.filterInbox("Starred") inboxPage.selectConversations(listOf(conversation1.subject!!, conversation2.subject!!)) + inboxPage.assertSelectedConversationNumber("2") inboxPage.clickUnstar() inboxPage.assertConversationNotDisplayed(conversation1.subject!!) inboxPage.assertConversationNotDisplayed(conversation2.subject!!) @@ -163,23 +130,23 @@ class InboxPageTest: TeacherTest() { @Test @TestMetaData(Priority.COMMON, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun starMultipleConversationWithDifferentStates() { + fun testInbox_starMultipleConversationWithDifferentStates() { val data = createInitialData() val conversation1 = data.addConversation( - senderId = data.students.first().id, - receiverIds = listOf(data.teachers.first().id), + senderId = getOtherUser().id, + receiverIds = listOf(getLoggedInUser().id), messageBody = "Body", messageSubject = "Subject") data.conversations[conversation1.id] = conversation1.copy(isStarred = true) val conversation2 = data.addConversation( - senderId = data.students.first().id, - receiverIds = listOf(data.teachers.first().id), + senderId = getOtherUser().id, + receiverIds = listOf(getLoggedInUser().id), messageBody = "Body 2", messageSubject = "Subject 2") data.conversations[conversation2.id] = conversation2.copy(isStarred = false) - data.addConversations(userId = data.teachers.first().id, messageBody = "Short body") + data.addConversations(userId = getLoggedInUser().id, messageBody = "Short body") - navigateToInbox(data, data.teachers.first()) + goToInbox(data) inboxPage.selectConversations(listOf(conversation1.subject!!, conversation2.subject!!)) inboxPage.assertSelectedConversationNumber("2") @@ -200,23 +167,25 @@ class InboxPageTest: TeacherTest() { @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun markAsReadUnreadMultipleConversations() { + fun testInbox_markAsReadUnreadMultipleConversations() { val data = createInitialData() - data.addConversations(userId = data.teachers.first().id, messageBody = "Short body") + data.addConversations(userId = getLoggedInUser().id, messageBody = "Short body") val conversation1 = data.addConversation( - senderId = data.students.first().id, - receiverIds = listOf(data.teachers.first().id), + senderId = getOtherUser().id, + receiverIds = listOf(getLoggedInUser().id), messageBody = "Body", messageSubject = "Subject") val conversation2 = data.addConversation( - senderId = data.students.first().id, - receiverIds = listOf(data.teachers.first().id), + senderId = getOtherUser().id, + receiverIds = listOf(getLoggedInUser().id), messageBody = "Body 2", messageSubject = "Subject 2") - navigateToInbox(data, data.teachers.first()) + goToInbox(data) + inboxPage.selectConversations(listOf(conversation1.subject!!, conversation2.subject!!)) inboxPage.assertSelectedConversationNumber("2") + inboxPage.clickMarkAsRead() inboxPage.assertUnreadMarkerVisibility(conversation1.subject!!, ViewMatchers.Visibility.GONE) inboxPage.assertUnreadMarkerVisibility(conversation2.subject!!, ViewMatchers.Visibility.GONE) @@ -227,9 +196,9 @@ class InboxPageTest: TeacherTest() { inboxPage.assertUnreadMarkerVisibility(conversation2.subject!!, ViewMatchers.Visibility.VISIBLE) inboxPage.assertEditToolbarIs(ViewMatchers.Visibility.VISIBLE) - inboxPage.selectConversation(conversation1.subject!!) - inboxPage.assertSelectedConversationNumber("1") + inboxPage.selectConversation(conversation1) inboxPage.clickMarkAsRead() + inboxPage.assertUnreadMarkerVisibility(conversation1.subject!!, ViewMatchers.Visibility.VISIBLE) inboxPage.assertUnreadMarkerVisibility(conversation2.subject!!, ViewMatchers.Visibility.GONE) inboxPage.assertEditToolbarIs(ViewMatchers.Visibility.VISIBLE) @@ -239,35 +208,32 @@ class InboxPageTest: TeacherTest() { inboxPage.assertUnreadMarkerVisibility(conversation1.subject!!, ViewMatchers.Visibility.GONE) inboxPage.assertUnreadMarkerVisibility(conversation2.subject!!, ViewMatchers.Visibility.GONE) - inboxPage.assertEditToolbarIs(ViewMatchers.Visibility.VISIBLE) - - inboxPage.clickMarkAsUnread() - - inboxPage.assertUnreadMarkerVisibility(conversation1.subject!!, ViewMatchers.Visibility.VISIBLE) - inboxPage.assertUnreadMarkerVisibility(conversation2.subject!!, ViewMatchers.Visibility.VISIBLE) } @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun deleteMultipleConversations() { + fun testInbox_deleteMultipleConversations() { val data = createInitialData() - data.addConversations(userId = data.teachers.first().id, messageBody = "Short body") + data.addConversations(userId = getLoggedInUser().id, messageBody = "Short body") val conversation1 = data.addConversation( - senderId = data.students.first().id, - receiverIds = listOf(data.teachers.first().id), + senderId = getOtherUser().id, + receiverIds = listOf(getLoggedInUser().id), messageBody = "Body", messageSubject = "Subject") val conversation2 = data.addConversation( - senderId = data.students.first().id, - receiverIds = listOf(data.teachers.first().id), + senderId = getOtherUser().id, + receiverIds = listOf(getLoggedInUser().id), messageBody = "Body 2", messageSubject = "Subject 2") - navigateToInbox(data, data.teachers.first()) + goToInbox(data) + inboxPage.selectConversations(listOf(conversation1.subject!!, conversation2.subject!!)) inboxPage.assertSelectedConversationNumber("2") + inboxPage.clickDelete() inboxPage.confirmDelete() + inboxPage.assertConversationNotDisplayed(conversation1.subject!!) inboxPage.assertConversationNotDisplayed(conversation2.subject!!) inboxPage.assertEditToolbarIs(ViewMatchers.Visibility.GONE) @@ -275,15 +241,16 @@ class InboxPageTest: TeacherTest() { @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun swipeToReadUnread() { + fun testInbox_swipeToReadUnread() { val data = createInitialData() - val conversation = data.addConversation( - senderId = data.students.first().id, - receiverIds = listOf(data.teachers.first().id), + data.addConversations(userId = getLoggedInUser().id, messageBody = "Short body") + val conversation = data.addConversation( + senderId = getOtherUser().id, + receiverIds = listOf(getLoggedInUser().id), messageBody = "Body", messageSubject = "Subject") - data.addConversations(userId = data.teachers.first().id, messageBody = "Short body") - navigateToInbox(data, data.teachers.first()) + + goToInbox(data) inboxPage.swipeConversationRight(conversation) inboxPage.assertUnreadMarkerVisibility(conversation.subject!!, ViewMatchers.Visibility.GONE) @@ -293,87 +260,86 @@ class InboxPageTest: TeacherTest() { @Test @TestMetaData(Priority.COMMON, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun swipeGesturesInUnreadScope() { + fun testInbox_swipeGesturesInUnreadScope() { val data = createInitialData() val conversation = data.addConversation( - senderId = data.teachers.first().id, - receiverIds = listOf(data.students.first().id), + senderId = getOtherUser().id, + receiverIds = listOf(getLoggedInUser().id), messageBody = "Unread body", messageSubject = "Unread Subject") - val unreadConversation = data.createBasicConversation(data.teachers.first().id, workflowState = Conversation.WorkflowState.UNREAD, messageBody = "Unread Body 2") + val unreadConversation = data.createBasicConversation(getOtherUser().id, workflowState = Conversation.WorkflowState.UNREAD, messageBody = "Unread Body 2") data.conversations[unreadConversation.id] = unreadConversation - navigateToInbox(data, data.teachers.first()) - inboxPage.filterMessageScope("Unread") + goToInbox(data) + inboxPage.filterInbox("Unread") inboxPage.swipeConversationRight(conversation) inboxPage.assertConversationNotDisplayed(conversation.subject!!) inboxPage.swipeConversationLeft(unreadConversation) inboxPage.assertConversationNotDisplayed(unreadConversation.subject!!) - inboxPage.filterMessageScope("Archived") + inboxPage.filterInbox("Archived") inboxPage.assertConversationDisplayed(unreadConversation.subject!!) } @Test @TestMetaData(Priority.NICE_TO_HAVE, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun swipeGesturesInSentScope() { + fun testInbox_swipeGesturesInSentScope() { val data = createInitialData() val sentConversation = data.addConversation( - senderId = data.teachers.first().id, - receiverIds = listOf(data.students.first().id), + senderId = getLoggedInUser().id, + receiverIds = listOf(getOtherUser().id), messageBody = "Sent body", messageSubject = "Sent Subject") - val sentConversation2 = data.createBasicConversation(data.students.first().id, messageBody = "Sent Body 2") + val sentConversation2 = data.createBasicConversation(getLoggedInUser().id, messageBody = "Sent Body 2") data.conversations[sentConversation2.id] = sentConversation2 - navigateToInbox(data, data.teachers.first()) - inboxPage.filterMessageScope("Sent") + goToInbox(data) + inboxPage.filterInbox("Sent") inboxPage.swipeConversationRight(sentConversation) inboxPage.assertUnreadMarkerVisibility(sentConversation.subject!!, ViewMatchers.Visibility.GONE) inboxPage.swipeConversationRight(sentConversation) inboxPage.assertUnreadMarkerVisibility(sentConversation.subject!!, ViewMatchers.Visibility.VISIBLE) - inboxPage.filterMessageScope("Unread") + inboxPage.filterInbox("Unread") inboxPage.assertConversationDisplayed(sentConversation.subject!!) } @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun swipeToArchive() { + fun testInbox_swipeToArchive() { val data = createInitialData() val conversation = data.addConversation( - senderId = data.students.first().id, - receiverIds = listOf(data.teachers.first().id), + senderId = getOtherUser().id, + receiverIds = listOf(getLoggedInUser().id), messageBody = "Body", messageSubject = "Subject") - data.addConversations(userId = data.teachers.first().id, messageBody = "Short body") - navigateToInbox(data, data.teachers.first()) + goToInbox(data) inboxPage.swipeConversationLeft(conversation) inboxPage.assertConversationNotDisplayed(conversation.subject!!) - inboxPage.filterMessageScope("Archived") + inboxPage.filterInbox("Archived") inboxPage.assertConversationDisplayed(conversation.subject!!) } @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun swipeGesturesInArchivedScope() { + fun testInbox_swipeGesturesInArchivedScope() { val data = createInitialData() val conversation = data.addConversation( - senderId = data.students.first().id, - receiverIds = listOf(data.teachers.first().id), + senderId = getOtherUser().id, + receiverIds = listOf(getLoggedInUser().id), messageBody = "Body", messageSubject = "Subject") - val archivedConversation = data.createBasicConversation(data.students.first().id, subject = "Archived subject", workflowState = Conversation.WorkflowState.ARCHIVED, messageBody = "Body 2") + val archivedConversation = data.createBasicConversation(getOtherUser().id, subject = "Archived subject", workflowState = Conversation.WorkflowState.ARCHIVED, messageBody = "Body 2") data.conversations[archivedConversation.id] = archivedConversation - navigateToInbox(data, data.teachers.first()) + goToInbox(data) inboxPage.swipeConversationLeft(conversation) - inboxPage.filterMessageScope("Archived") + inboxPage.filterInbox("Archived") inboxPage.assertConversationDisplayed(conversation.subject!!) inboxPage.swipeConversationRight(conversation) inboxPage.assertConversationNotDisplayed(conversation.subject!!) //Because an Unread conversation cannot be Archived. @@ -381,7 +347,7 @@ class InboxPageTest: TeacherTest() { inboxPage.swipeConversationLeft(archivedConversation) inboxPage.assertConversationNotDisplayed(archivedConversation.subject!!) - inboxPage.filterMessageScope("Inbox") + inboxPage.filterInbox("Inbox") inboxPage.assertConversationDisplayed(conversation.subject!!) inboxPage.assertUnreadMarkerVisibility(conversation.subject!!, ViewMatchers.Visibility.VISIBLE) inboxPage.assertConversationDisplayed(archivedConversation.subject!!) @@ -389,19 +355,18 @@ class InboxPageTest: TeacherTest() { @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) - fun swipeToUnstar() { + fun testInbox_swipeToUnstar() { val data = createInitialData() val conversation = data.addConversation( - senderId = data.students.first().id, - receiverIds = listOf(data.teachers.first().id), + senderId = getOtherUser().id, + receiverIds = listOf(getLoggedInUser().id), messageBody = "Body", messageSubject = "Subject") - data.addConversations(userId = data.teachers.first().id, messageBody = "Short body") + data.addConversations(userId = getLoggedInUser().id, messageBody = "Short body") data.conversations[conversation.id] = conversation.copy(isStarred = true) - navigateToInbox(data, data.teachers.first()) - inboxPage.filterMessageScope("Starred") - + goToInbox(data) + inboxPage.filterInbox("Starred") inboxPage.assertUnreadMarkerVisibility(conversation.subject!!, ViewMatchers.Visibility.VISIBLE) inboxPage.swipeConversationRight(conversation) inboxPage.assertUnreadMarkerVisibility(conversation.subject!!, ViewMatchers.Visibility.GONE) @@ -412,18 +377,101 @@ class InboxPageTest: TeacherTest() { inboxPage.assertConversationNotDisplayed(conversation.subject!!) } - private fun createInitialData(): MockCanvas { - return MockCanvas.init( - courseCount = 1, - favoriteCourseCount = 1, - teacherCount = 1, - studentCount = 1 - ) + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) + fun testInbox_filterMessagesByTypeAll() { + // Should be able to filter messages by All + val data = createInitialData() + data.addConversations(userId = getLoggedInUser().id, messageBody = "Short body") + val conversation = getFirstConversation(data) + goToInbox(data) + inboxPage.assertConversationDisplayed(conversation.subject!!) + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) + fun testInbox_filterMessagesByTypeUnread() { + // Should be able to filter messages by Unread + val data = createInitialData() + data.addConversations(userId = getLoggedInUser().id, messageBody = "Short body") + goToInbox(data) + val conversation = data.conversations.values.first { + it.workflowState == Conversation.WorkflowState.UNREAD + } + inboxPage.filterInbox("Unread") + inboxPage.assertConversationDisplayed(conversation.subject!!) + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) + fun testInbox_filterMessagesByTypeStarred() { + // Should be able to filter messages by Starred + val data = createInitialData() + data.addConversations(userId = getLoggedInUser().id, messageBody = "Short body") + goToInbox(data) + val conversation = data.conversations.values.first { + it.isStarred + } + inboxPage.filterInbox("Starred") + inboxPage.assertConversationDisplayed(conversation.subject!!) + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) + fun testInbox_filterMessagesByTypeSend() { + // Should be able to filter messages by Send + val data = createInitialData() + data.addConversations(userId = getLoggedInUser().id, messageBody = "Short body") + goToInbox(data) + val conversation = data.conversations.values.first { + it.workflowState == Conversation.WorkflowState.UNREAD + } + inboxPage.filterInbox("Sent") + inboxPage.assertConversationDisplayed(conversation.subject!!) + } + + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) + fun testInbox_filterMessagesByTypeArchived() { + // Should be able to filter messages by Archived + val data = createInitialData() + data.addConversations(userId = getLoggedInUser().id, messageBody = "Short body") + goToInbox(data) + val conversation = data.conversations.values.first { + it.workflowState == Conversation.WorkflowState.ARCHIVED + } + inboxPage.filterInbox("Archived") + inboxPage.assertConversationDisplayed(conversation.subject!!) } - private fun navigateToInbox(data: MockCanvas, teacher: User) { - val token = data.tokenFor(teacher)!! - tokenLogin(data.domain, token, teacher) - dashboardPage.clickInboxTab() + @Test + @TestMetaData(Priority.IMPORTANT, FeatureCategory.INBOX, TestCategory.INTERACTION) + fun testInbox_filterMessagesByContext() { + // Should be able to filter messages by course or group + val data = createInitialData(courseCount = 2) + data.addConversationsToCourseMap(getLoggedInUser().id, data.courses.values.toList(), messageBody = "Short body") + val firstCourse = data.courses.values.first() + val conversation = data.conversationCourseMap[firstCourse.id]!!.first() + goToInbox(data) + inboxPage.selectInboxFilter(firstCourse) + inboxPage.assertConversationDisplayed(conversation.subject!!) + } + + private fun getFirstConversation(data: MockCanvas, includeIsAuthor: Boolean = false): Conversation { + return data.conversations.values.toList() + .filter { it.workflowState != Conversation.WorkflowState.ARCHIVED } + .first { + if (includeIsAuthor) it.messages.first().authorId == getLoggedInUser().id else it.messages.first().authorId != getLoggedInUser().id + } } -} + + override fun displaysPageObjects() = Unit + + abstract fun goToInbox(data: MockCanvas) + + abstract fun createInitialData(courseCount: Int = 1): MockCanvas + + abstract fun getLoggedInUser(): User + + abstract fun getOtherUser(): User +} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/InboxPage.kt similarity index 89% rename from apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt rename to automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/InboxPage.kt index f3fa937f0e..660f4ab022 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/InboxPage.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/pages/InboxPage.kt @@ -14,7 +14,7 @@ * limitations under the License. * */ -package com.instructure.student.ui.pages +package com.instructure.canvas.espresso.common.pages import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu import androidx.test.espresso.action.ViewActions @@ -31,7 +31,6 @@ import com.instructure.canvas.espresso.scrollRecyclerView import com.instructure.canvas.espresso.waitForMatcherWithRefreshes import com.instructure.canvasapi2.models.Conversation import com.instructure.canvasapi2.models.Course -import com.instructure.dataseeding.model.ConversationApiModel import com.instructure.espresso.OnViewWithId import com.instructure.espresso.RecyclerViewItemCountGreaterThanAssertion import com.instructure.espresso.WaitForViewWithId @@ -51,23 +50,18 @@ import com.instructure.espresso.page.withText import com.instructure.espresso.scrollTo import com.instructure.espresso.swipeLeft import com.instructure.espresso.swipeRight -import com.instructure.student.R +import com.instructure.pandautils.R import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.not class InboxPage : BasePage(R.id.inboxPage) { - private val toolbar by OnViewWithId(R.id.toolbar) private val createMessageButton by OnViewWithId(R.id.addMessage) private val scopeButton by OnViewWithId(R.id.scopeFilter) private val filterButton by OnViewWithId(R.id.courseFilter) private val inboxRecyclerView by WaitForViewWithId(R.id.inboxRecyclerView) private val editToolbar by OnViewWithId(R.id.editToolbar, autoAssert = false) - fun assertConversationDisplayed(conversation: ConversationApiModel) { - assertConversationDisplayed(conversation.subject) - } - fun assertConversationDisplayed(subject: String) { val matcher = withText(subject) onView(matcher).scrollTo().assertDisplayed() @@ -78,10 +72,6 @@ class InboxPage : BasePage(R.id.inboxPage) { onView(matcher).scrollTo().assertDisplayed() } - fun assertConversationNotDisplayed(conversation: ConversationApiModel) { - assertConversationNotDisplayed(conversation.subject) - } - fun assertConversationNotDisplayed(subject: String) { val matcher = withText(subject) onView(matcher).check(doesNotExist()) @@ -105,10 +95,6 @@ class InboxPage : BasePage(R.id.inboxPage) { onView(matcher).scrollTo().click() } - fun openConversation(conversation: ConversationApiModel) { - openConversation(conversation.subject) - } - fun openConversation(conversation: Conversation) { waitForView(withId(R.id.inboxRecyclerView)) val matcher = withText(conversation.subject) @@ -128,12 +114,18 @@ class InboxPage : BasePage(R.id.inboxPage) { waitForViewWithText(course.name).click() } - fun pressNewMessageButton() { - createMessageButton.click() + fun selectInboxFilter(courseName: String) { + filterButton.click() + waitForViewWithText(courseName).click() } - fun goToDashboard() { - onView(withId(R.id.bottomNavigationHome)).click() + fun clearCourseFilter() { + waitForView(withId(R.id.courseFilter)).click() + onView(withId(R.id.clear) + withText(R.string.inboxClearFilter)).click() + } + + fun pressNewMessageButton() { + createMessageButton.click() } fun assertConversationStarred(subject: String) { @@ -158,9 +150,9 @@ class InboxPage : BasePage(R.id.inboxPage) { fun assertUnreadMarkerVisibility(conversation: Conversation, visibility: ViewMatchers.Visibility) { val matcher = allOf( - withId(R.id.unreadMark), - withEffectiveVisibility(visibility), - hasSibling(allOf(withId(R.id.message), withText(conversation.lastMessage)))) + withId(R.id.unreadMark), + withEffectiveVisibility(visibility), + hasSibling(allOf(withId(R.id.message), withText(conversation.lastMessage)))) if(visibility == ViewMatchers.Visibility.VISIBLE) { waitForMatcherWithRefreshes(matcher) // May need to refresh before the unread mark shows up @@ -211,10 +203,6 @@ class InboxPage : BasePage(R.id.inboxPage) { selectConversation(conversation.subject!!) } - fun selectConversation(conversation: ConversationApiModel) { - selectConversation(conversation.subject!!) - } - fun clickArchive() { waitForViewWithId(R.id.inboxArchiveSelected).click() } @@ -262,10 +250,6 @@ class InboxPage : BasePage(R.id.inboxPage) { onView(matcher).scrollTo().swipeRight() } - fun swipeConversationRight(conversation: ConversationApiModel) { - swipeConversationRight(conversation.subject!!) - } - fun swipeConversationRight(conversation: Conversation) { swipeConversationRight(conversation.subject!!) } @@ -281,10 +265,6 @@ class InboxPage : BasePage(R.id.inboxPage) { swipeConversationLeft(conversation.subject!!) } - fun swipeConversationLeft(conversation: ConversationApiModel) { - swipeConversationLeft(conversation.subject!!) - } - fun selectConversations(conversations: List) { refresh() for(conversation in conversations) { @@ -304,4 +284,12 @@ class InboxPage : BasePage(R.id.inboxPage) { onView(withId(R.id.subjectView) + withText(expectedSubject) + withAncestor(R.id.inboxRecyclerView)).assertDisplayed() } + fun refreshInbox() { + refresh() + } + + fun assertThereIsAnUnreadMessage(unread: Boolean) { + if(unread) onView(withId(R.id.unreadMark)).assertDisplayed() + else onView(withId(R.id.unreadMark) + withEffectiveVisibility(ViewMatchers.Visibility.GONE)) + } } \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt index 9d2c2d05cf..26e9a14cb2 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/CourseAPI.kt @@ -114,6 +114,9 @@ object CourseAPI { @GET("courses?state[]=completed&state[]=available&state[]=unpublished") fun getCoursesByEnrollmentType(@Query("enrollment_type") type: String): Call> + @GET("courses?state[]=completed&state[]=available") + suspend fun getCoursesByEnrollmentType(@Query("enrollment_type") type: String, @Tag params: RestParams): DataResult> + // TODO: Set up pagination when API is fixed and remove per_page query parameterø @GET("courses/{courseId}/grading_periods?per_page=100") fun getGradingPeriodsForCourse(@Path("courseId") courseId: Long): Call diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UnreadCountAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UnreadCountAPI.kt index c7b54c8bff..9acfa5b720 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UnreadCountAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UnreadCountAPI.kt @@ -6,16 +6,21 @@ import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.models.UnreadConversationCount import com.instructure.canvasapi2.models.UnreadCount import com.instructure.canvasapi2.models.UnreadNotificationCount +import com.instructure.canvasapi2.utils.DataResult import retrofit2.Call import retrofit2.http.GET import retrofit2.http.Query +import retrofit2.http.Tag -internal object UnreadCountAPI { - internal interface UnreadCountsInterface { +object UnreadCountAPI { + interface UnreadCountsInterface { @GET("conversations/unread_count") fun getUnreadConversationCount(): Call + @GET("conversations/unread_count") + suspend fun getUnreadConversationCount(@Tag params: RestParams): DataResult + @GET("users/self/activity_stream/summary?only_active_courses=true") fun getNotificationsCount(): Call> diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/ApiModule.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/ApiModule.kt index cc0f676d47..d9821350e8 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/ApiModule.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/ApiModule.kt @@ -261,4 +261,9 @@ class ApiModule { fun provideThemeApi(): ThemeAPI.ThemeInterface { return RestBuilder().build(ThemeAPI.ThemeInterface::class.java, RestParams()) } + + @Provides + fun provideUnreadCountApi(): UnreadCountAPI.UnreadCountsInterface { + return RestBuilder().build(UnreadCountAPI.UnreadCountsInterface::class.java, RestParams()) + } } \ No newline at end of file diff --git a/libs/pandares/src/main/res/drawable/bg_button_full_rounded_filled.xml b/libs/pandares/src/main/res/drawable/bg_button_full_rounded_filled.xml new file mode 100644 index 0000000000..40c861921e --- /dev/null +++ b/libs/pandares/src/main/res/drawable/bg_button_full_rounded_filled.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/libs/pandares/src/main/res/drawable/bg_button_full_rounded_filled_with_border.xml b/libs/pandares/src/main/res/drawable/bg_button_full_rounded_filled_with_border.xml new file mode 100644 index 0000000000..5bdbb11e75 --- /dev/null +++ b/libs/pandares/src/main/res/drawable/bg_button_full_rounded_filled_with_border.xml @@ -0,0 +1,25 @@ + + + + + + + \ No newline at end of file diff --git a/libs/pandares/src/main/res/values/strings.xml b/libs/pandares/src/main/res/values/strings.xml index 717dc3e750..21ab35b434 100644 --- a/libs/pandares/src/main/res/values/strings.xml +++ b/libs/pandares/src/main/res/values/strings.xml @@ -1760,4 +1760,6 @@ Canvas Student TEACHER Canvas Teacher + 99+ + Open navigation drawer, %s unread messages diff --git a/libs/pandautils/src/main/res/layout/fragment_inbox.xml b/libs/pandautils/src/main/res/layout/fragment_inbox.xml index cc82d34096..d027dfb322 100644 --- a/libs/pandautils/src/main/res/layout/fragment_inbox.xml +++ b/libs/pandautils/src/main/res/layout/fragment_inbox.xml @@ -37,7 +37,8 @@ + android:layout_height="wrap_content" + android:elevation="6dp" > Date: Mon, 15 Jul 2024 14:20:16 +0200 Subject: [PATCH 07/50] [MBL-17642][All] Upgrade AGP to 7.4.2 (#2494) refs: MBL-17642 affects: All release note: none * Update AGP to 7.4.2 * Added default English strings. * Fixed inbox test. --- .../student/ui/e2e/InboxE2ETest.kt | 1 + .../src/main/res/values-en/strings.xml | 903 +++++++++ buildSrc/build.gradle.kts | 2 +- buildSrc/src/main/java/GlobalDependencies.kt | 2 +- .../src/main/res/values-en/strings.xml | 41 + .../src/main/res/values-en/strings.xml | 80 + .../src/main/res/values-en/strings.xml | 133 ++ .../src/main/res/values-en/strings.xml | 1763 +++++++++++++++++ .../src/main/res/values-en/strings.xml | 490 +++++ 9 files changed, 3413 insertions(+), 2 deletions(-) create mode 100644 apps/teacher/src/main/res/values-en/strings.xml create mode 100644 libs/annotations/src/main/res/values-en/strings.xml create mode 100644 libs/canvas-api-2/src/main/res/values-en/strings.xml create mode 100644 libs/login-api-2/src/main/res/values-en/strings.xml create mode 100644 libs/pandares/src/main/res/values-en/strings.xml create mode 100644 libs/pandautils/src/main/res/values-en/strings.xml diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt index 89410e1dae..fd562e1826 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/InboxE2ETest.kt @@ -156,6 +156,7 @@ class InboxE2ETest: StudentTest() { Log.d(STEP_TAG,"Navigate to 'INBOX' scope and assert that '${seededConversation.subject}' conversation is NOT displayed because it is archived yet.") inboxPage.filterInbox("Inbox") + sleep(2000) inboxPage.assertConversationNotDisplayed(seededConversation.subject) Log.d(STEP_TAG, "Navigate to 'ARCHIVED' scope and Select the conversation. Star it, and assert that it has displayed in the 'STARRED' scope.") diff --git a/apps/teacher/src/main/res/values-en/strings.xml b/apps/teacher/src/main/res/values-en/strings.xml new file mode 100644 index 0000000000..d5cdfce8ab --- /dev/null +++ b/apps/teacher/src/main/res/values-en/strings.xml @@ -0,0 +1,903 @@ + + + + + Settings + Week + Inbox + Details + Questions + Submissions + Assignments + Profile + People + Quizzes + Discussions + Announcements + Attendance + Pages + To Do + School Logo + Search the Canvas Guides + Canvas Guides + Icon for Canvas Guides + Answers to frequently asked questions + Report a Problem + Icon for Report a Problem + If the app misbehaves, let us know + Request a Feature + Icon for Request a Feature + Let us know your idea to improve the app + Share Your Love for the App + Tell us about your favorite parts of the app + Masquerade + Issue with Teacher App [Android] + No items + Rate the app + Icon for Rate the app + Log Out + Switch User + Under Construction + Event + Location + Event Date + Course + Profile + Share + Share With… + + + Open navigation drawer + Close navigation drawer + + + (no subject) + Compose message + New Message + Courses + Compose Message + Send + Attachment + Attachments + Functionality unavailable while offline + The message cannot be blank! + This message currently has no recipients. + Course: + Reply + Forward Message + To + Course + Subject + + Send + Edit + + + Idea for Teacher App [Android] + The following information will help us better understand your idea: + Send Email… + + Red + Hot Pink + Lavender + Violet + Purple + Slate + Blue + Cyan + Green + Chartreuse + Yellow + Gold + Orange + Pink + Gray + + Attachment Icon + Star Icon + Monologue + + Topic Title + Post Date + Announcement + New Announcement + A description is required + Announcement Details + + No Title + Announcement Created! + Unable to create announcement. Please check your connection and try again. + Delay Posting + + Discussion + + Sunday + Monday + Tuesday + Wednesday + Thursday + Friday + Saturday + + Add Attachment + File icon + Next + Upload To Canvas + My Files + Course Files + Upload To + Message Attachments + with + Attach a file + Select files or photos + Uploading files… + (No Attachment) + Creating Announcement… + + Copy email address + All Grading Periods + No submissions + All submissions graded + icon + + %d submission needs grading + %d submissions need grading + + + + There is %d assignee without a grade. + There are %d assignees without grades. + + + No messages + No Starred Messages + Tap the \"+\" to create a new conversation. + Star messages by tapping the star in the message. + + Reply + Reply All + Forward + Star + Archive + Delete + Message + Unarchive + Message Options + Mark As Unread + Are you sure you want to delete your copy of this message? This action cannot be undone. + Are you sure you want to delete your copy of this conversation? This action cannot be undone. + Unable to perform this action. Please check your connection and try again. + Message archived + Message unarchived + Message deleted + Select Recipients + Entire group selected + No users in group + + Published + Unpublished + Published check mark + points + point + No Due Date + Due Date + + View Replies + View Discussion + + Edit + Save + Successfully updated assignment. + Successfully updated quiz. + Something went wrong updating the assignment. Try again. + Something went wrong deleting the assignment. Try again. + Can\'t unpublish if there are student submissions. + Messages + + + %d Message + + %d Messages + + + %d submission needs grading + %d submissions need grading + + + Are you sure you want to delete this assignment? + + 99+ + Message Student + + Message these students + Missing + Late + Submitted + Not submitted yet + Excused + Last login: %s + + Turn on Anonymous Grading + Turn off Anonymous Grading + Anonymous Grading + Student + + + Feature Coming Soon + + + All Submissions + Late Submissions + Missing Submissions + Grade High + Grade Low + + + + By Name + Grade High to Low + Grade Low to High + + + Course Tab + Inbox Tab + Profile Tab + Close + Please select a course + Discussion Reply + Announcement Reply + Add some text to send a message + This message could not be sent. Tap to try again. + Message sent successfully. + An unexpected error occurred. + This course has no assignments that allow file uploads. + The selected file type is not allowed. + Allowed extensions:  + Files uploaded successfully. + An error occurred uploading your file. Please try again. + Uploading file… + Your device doesn\'t have any applications installed that can handle this file. + Turn in + There was an error getting the photo. Please try again. + Loading Files… + You haven\'t selected any files. + You can\'t upload files to the selected assignment. + One or more files has a file extension that isn\'t allowed + + Inbox + Unread + Starred + Sent + Archived + All + Uploading %d of %d + Page not found. + No Connection + You are unauthorized to access this. Either you don\'t have permissions or you don\'t have access yet (like your course hasn\'t started yet) + A server error has occurred. + An error occurred loading your file. Please check your internet connection and try again. + Files submitted successfully. + Please check your data connection and try again. + Are you sure you want to log out? + Yes + No + Sending… + Cancel + Okay + Upload + Settings + Edit Courses + Start Annotating + Empty Speedgrader View, Need Design + + + Course Settings + Course Name + Set \'Home\' to… + Course Activity Stream + Pages Front Page + Front Page + Course Modules + Assignments List + Syllabus + + + + Multiple Due Dates + Everyone + Everyone else + Unknown Student + Closed + -- + Due %1s at %2s + Last post %s + Submission Types + Full due date details + Availability: + Available to: + Available from: + For: + Due: + Available to + Available from + For + Due + Assignment Details + No Content + Submissions + Help your students with this assignment by adding instructions. + Help your students with this quiz by adding instructions. + Due Dates + Edit Assignment + Edit Quiz + Title + Description + Grade Total + Grade + Enter Grade + Override + Calculated by rubric + Customize Grade + Out of %1$s + GPA + Letter Grade + Excuse student + Excuse group + Complete + Incomplete + Not Graded + Excused + %s %s + %1$s out of %2$s + Publish + An error occurred trying to save the assignment. Try again. + An error occurred trying to save the quiz. Try again. + Assignment name must be set. + Announcement title must be set. + Quiz title must be set. + Assignment points must be set. + Points possible must be a number + Quiz points must be set. + Course Sections + Groups + Students + Add Assignees + Assign To + Available From + Available To + Unlock date cannot be after due date + Lock date cannot be before due date + Lock date cannot be before unlock date + Assignee cannot be blank + Remove + Add Due Date + Full submission details + Graded + Needs Grading + Not Submitted + view submission details + Graded, %s of %s + Needs Grading, %s of %s + Not submitted, %s of %s + Display Grade as… + Percentage + Complete/Incomplete + Points + GPA Scale + + + @string/percentage + @string/complete_incomplete + @string/points + @string/letter_grade + @string/gpa_scale + @string/not_graded + + + + @string/percentage + @string/complete_incomplete + @string/points + @string/letter_grade + @string/gpa_scale + + + Not a teacher? + One of our other apps might be a better fit. Tap one to visit the Play Store. + + Grade + Comments + Files + Files (%d) + Filter Submissions + All Submissions + Submitted Late + Haven\'t Submitted Yet + Haven\'t Been Graded + Scored Less Than… + Scored More Than… + Scored Less Than %s + Scored More Than %s + Add Comment + Edit Comment + Delete Comment + Delete Annotation + Are you sure you want to delete this comment? + View long description + Rubric + Enter a custom value + Rubric assessment saved + An error occurred while saving the rubric assessment. Please try again. + %1$s out of %2$s + Customize Score + Edit criterion comment + Message %s + Save comment + Cancel editing comment + Skip + + + Custom score %1$s + + + "%1$s, %2$s + + + out of %s pt + out of %s pts + + + Courses + Course Options + Edit Favorite Courses + Feedback Form + See All + All Courses + Welcome! + Add a few of your favorite courses to make this place your home. + Add Courses + This course cannot be added to the courses menu at this time. + Grading Periods Filter + Assignments + Clear filter + Due %1$s + Updated %1$s + + + %s needs grading + %s need grading + + + Needs Grading + Need Grading + + at + + %s pt + %s pts + + + %s point + %s points + + + %d person + %d people + + + %d group + %d groups + + Closed + %s %s + favorite + not favorite + Edit nickname + Edit course color + Edit Course Nickname + + Course Color + This is your personal color setting. Only you will see this color for the course. + The course color could not be set at this time. + + Pink + Hot Pink + Violet + Purple + Dark Blue + Blue + Cyan + Aqua Blue + Emerald Green + Green + Chartreuse + Yellow + Orange + Dark Orange + Red + Launch link in external browser + Selected File + File Icon + Expand Page + Collapse Page + No Internet Connection + This action requires an internet connection. + + Tap on an item in the list to view the details + + Assignment Quizzes + Practice Quizzes + Graded Surveys + Surveys + Edit Quiz Details + Require an Access Code + Access Code + Enter an access code or do not require an access code. + An error occurred trying to save the quiz. Try again. + Successfully updated quiz. + Quiz Type + + Practice Quiz + Graded Quiz + Graded Survey + Ungraded Survey + + + @string/practiceQuiz + @string/gradedQuiz + @string/gradedSurvey + @string/ungradedSurvey + + Untaken + + Complete + Pending Review + In Progress + Not Started + + + Quiz Type: + Assignment Group: + Shuffle Answers: + Multiple Attempts: + Time Limit: + Score to Keep: + Attempts: + View Responses: + Show Correct Answers: + One Question At a Time: + IP Filter: + Points: + Access Code: + Anonymous Submissions: + Lock Questions After Answering: + + Quiz Preview + Preview Quiz + Quiz Details + Submission versions + This student has no submissions for this assignment. + This group has no submissions for this assignment. + Unsupported submission type + Unlimited + Practice Quiz + Graded Quiz + Graded Survey + Ungraded Survey + No Time Limit + Immediately + No points assigned + + From %1$s to %2$s + After %1$s + Until %1$s + Enter a value. + + + %d question + %d questions + + + Discussion Details + Pinned + Discussions + Closed for Comments + Open for Comments + Pin + Unpin + More options + Options + %s Replies + %s Unread + %s %s %s + Are you sure you want to delete this reply? + Replies + Reply + Edit + Replies are only visible to those who have posted at least one reply. + Delete Discussion + Delete Discussion? + This will delete the whole discussion and thread. + + Data Usage Warning + This action might consume a large amount of data, potentially incurring overage charges. Would you like to continue? + Do not show this message again + Play media + This submission is a media file + Try again + This media format is not supported + Open with… + + Assignment submitted. Tap to view. + Submitted files: + There are no submission comments + Media Upload - Audio + Media Upload - Video + Attempt %d + Text Submission + External Tool Submission + Discussion Submission + Quiz Submission + Media File + Audio + Video + URL Submission + + This file could not be displayed. Use the button below to open the file with another app on your device. + This assignment does not allow submissions. + This assignment only allows on-paper submissions. + This submission was a URL to an external page. Keep in mind that this page may have changed since the submission originally occurred. + Open URL + Annotation Error + The annotation has been deleted from another source. + + Select a course + + Send individual message to each recipient + + Filter Inbox + Select a course or group + + Go Back + + The discussion edit was successful. + The discussion message cannot be empty. + The discussion was unable to be edited at this time. + The discussion reply was successful. + The discussion reply cannot be empty. + The discussion reply was unable to be sent at this time. + Be the first to respond by adding a reply. + Add another recipient. Messages addressed only to yourself cannot be sent. + Swipe left or right to view other students. + Tap and hold number to see description. + + View Submission + Grade Submission + Comment + Message Students Who… + + on + + New Discussion + Options + Subscribe + Allow threaded replies + Users must post before seeing replies + Allow users to comment + Discussion title must be set. + Discussion successfully created. + An error occurred trying to create this discussion. Try again. + Edit Discussion + Discussion successfully updated. + Input Text + Announcement successfully created. + An error occurred trying to save this announcement. Try again. + Edit Announcement + Announcement successfully updated. + Announcement deleted. + Post At + An error occurred trying to delete this announcement. Try again. + Delete Announcement? + Are you sure you want to delete this announcement? + Delete Announcement + An unexpected error occurred while loading annotations. + + Account + Change User + General + Send Feedback + Privacy Policy + EULA + Terms of Use + Search the Canvas Guides + Find answers to common questions + Report a Problem + If the app misbehaves, let us know + Request a Feature + Have an idea to improve the app? + Share Your Love for the App + Tell us about your favorite parts of the app + Canvas Guides + Idea for Canvas Teacher [Android] + The following information will help us better understand your idea: + Send Email… + Edit Profile + Name + Bio + Profile has been updated + Unable to update profile + Take photo + Choose photo from Gallery + File not found. + Version %s + Version %s (%d) + Tap to view submissions list. + Assignment Description + Quiz Description + Remove Due Date + This will remove the due date and all of the associated assignees. + Email + fullscreen + Add New + Send Message + Add Message + Message this student + Scroll to see all details + Due Time + Available From Date + Available From Time + Available To Date + Available To Time + Add Description + Post Time + All People + Loading + Saving + Sending + Uploading + Retry + There was a problem loading this submission. + Search + Filter People + Load More + Latest activity on %1$s at %2$s. + Unable to load details for this user. + Student + Teacher + Observer + TA + Designer + Unknown + Attendance could not be loaded at this time. + Mark Remaining as Present + Mark All as Present + Calendar + Filter Sections + Add video comment + Add audio comment + 00:00:00 + %1$d hours, %2$d minutes, and %3$d seconds + %1$s out of %2$s + Replay + Stop + Start recording audio + Stop recording audio + Start recording video + Stop recording video + Close recording view + Delete recording + Video Comment Replay + Add media comment + Copyright Holder + Access + Usage Rights + Delete File + I hold the copyright + I have permission to use file + Public Domain File + Fair Use Exception + Creative Commons File + Edit File + Delete Folder + Edit Folder + Are you sure you want to delete this file? + Are you sure you want to delete this folder? All contents of this folder will also be deleted. + New Folder + Create A Folder + Create Folder + Create A File + Show Create File and Create Folder Buttons + Hide Create File and Create Folder Buttons + Unpublish + Restricted Access + Restricted + Hidden, files inside will be available with links. + Only available to students with link. Not available in student files. + Schedule student availability + An error occurred during folder creation. + + Set as Front Page + Can Edit + Page Details + Page successfully updated. + Page successfully created. + Edit Page + Create Page + Page title must be set. + Delete Page? + Delete Page + This will delete the page. This action cannot be undone. + An error occurred trying to save this page. Try again. + A page cannot be the front page and unpublished. + + Only Teachers + Teachers and Students + Anyone + Only Members + License + Availability end date must be after start date + An error occurred deleting this file + An error occurred deleting this folder + An error occurred updating this file + An error occurred updating this folder + + Nothing more to do!\n Enjoy your day. + There are no submissions to grade for this assignment. + An error occurred trying to view this To Do item. + Designer enrollment cannot view this. + + Sections + Filter By… + for %s + Filter by section + Filter submissions + Late penalty + Final Grade + Moderated grading is not currently supported in mobile SpeedGrader. + + -%s pt + -%s pts + + + -%s point + -%s points + + Profile Settings + Downloading file… + File downloaded successfully. + Tap to view + An error occurred downloading your file. Please try again. + + Post To + All Sections + + Gauge + This LTI tool could not be loaded at this time. + Downloading… + + + Syllabus successfully updated. + An error occurred trying to save the syllabus. Try again. + Edit Syllabus + Content + Details + Show course summary + Syllabus content + No grade + Excuse + Overgraded by %s + Cannot unpublish %s if there are student submissions + + diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 6812b1a5be..1e04771629 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -19,7 +19,7 @@ repositories { mavenCentral() } -val agpVersion= "7.1.3" +val agpVersion= "7.4.2" dependencies { implementation("com.android.tools.build:gradle:$agpVersion") diff --git a/buildSrc/src/main/java/GlobalDependencies.kt b/buildSrc/src/main/java/GlobalDependencies.kt index f1d3573756..fef5638e5a 100644 --- a/buildSrc/src/main/java/GlobalDependencies.kt +++ b/buildSrc/src/main/java/GlobalDependencies.kt @@ -7,7 +7,7 @@ object Versions { const val TARGET_SDK = 34 /* Build/tooling */ - const val ANDROID_GRADLE_TOOLS = "7.1.3" + const val ANDROID_GRADLE_TOOLS = "7.4.2" const val BUILD_TOOLS = "34.0.0" /* Testing */ diff --git a/libs/annotations/src/main/res/values-en/strings.xml b/libs/annotations/src/main/res/values-en/strings.xml new file mode 100644 index 0000000000..f4fb2d0545 --- /dev/null +++ b/libs/annotations/src/main/res/values-en/strings.xml @@ -0,0 +1,41 @@ + + + + Input Text + Cancel + Comment + Skip + Comments + There was a problem loading this submission. + Retry + An unexpected error occurred while loading annotations. + Annotation Error + The annotation has been deleted from another source. + This message could not be sent. Tap to try again. + Add Comment + Edit Comment + Delete Comment + Delete Annotation + Are you sure you want to delete this comment? + Deleting this comment will delete all comments associated with this annotation. Are you sure you want to do this? + Sending + Note Color + Removed %1$s by %2$s + + + diff --git a/libs/canvas-api-2/src/main/res/values-en/strings.xml b/libs/canvas-api-2/src/main/res/values-en/strings.xml new file mode 100644 index 0000000000..7ce402651e --- /dev/null +++ b/libs/canvas-api-2/src/main/res/values-en/strings.xml @@ -0,0 +1,80 @@ + + + + + + + You must first complete: + It will be unlocked at: + Go To Modules + + + Starts + Ends + All Day Event + at + to + + + Message + Loading… + Deleted + + + Online + On Paper + Discussion + Quiz + External Tool + No Submission + + Pass Fail + Percent + Letter Grade + Points + GPA Scale + Not Graded + + Quiz + Discussion Topic + File Upload + Text Entry + Website URL + Media Recording + Attendance + Student Annotation + Highest + Average + Latest + No + Always + Graded Quiz + Practice Quiz + Graded Survey + Ungraded Survey + \u0020and %d more + + + Student + Teacher + Observer + TA + Designer + Unknown + + diff --git a/libs/login-api-2/src/main/res/values-en/strings.xml b/libs/login-api-2/src/main/res/values-en/strings.xml new file mode 100644 index 0000000000..af49f76146 --- /dev/null +++ b/libs/login-api-2/src/main/res/values-en/strings.xml @@ -0,0 +1,133 @@ + + + + + + Hello world! + Settings + This app is not authorized for use + The server you entered is not authorized for this app. + The user agent for this app is unauthorized. + We were unable to verify the server for use with this app. + UnknownDevice + An unexpected error occurred. + Page not found. + Confirm + Cancel + Help + Enter your Canvas URL: + Trying to sign into Canvas Network? + Remove + myschool.instructure.com + User ID + Domain + Act as User + Stop Acting as User + + + Remove this user? + You\'ll have to sign into this user again to access their content. + + + Canvas Logo + + Canvas_Login: ON + Canvas_Login: OFF + Site Admin Login: ON + + Username + Password + Authentication Required + Invalid email or password + Log In + Log Out + Matching Schools + + + Are you sure you want to logout? + Yes + No + + + + Near You + Canvas Network + Right behind you + Don\'t see your school? Enter the school URL or tap here for help. + Enter the school URL or tap here for help. + Find your school or district + + + Report A Problem + Subject + Description + Send + Thank you for reporting your issue to our Support Team. You will receive verification and updates via email. + How is this affecting you? + Just a casual question, comment, idea, suggestion… + I need some help but it\'s not urgent. + Something\'s broken but I can work around it to get what I need done. + I can\'t get things done until I hear back from you. + EXTREME CRITICAL EMERGENCY!! + + Email Address + Unknown + + + < 1 mile (1.6 km) + 1 mile (1.6 km) + miles + km + F + + Device + OS Version + + Previous Logins + Find my school + QR Code + Locate QR Code + You\'ll find the QR code on the web in your account profile. Click \'QR for Mobile Login\' in the list. + Scan a QR code from Canvas to login + There was an error logging in. Please generate another QR Code and try again. + Please scan a QR code generated by Canvas + Screenshot showing location of QR code generation in browser + What\'s your school\'s name? + Next + school.instructure.com + Remove Previous User + Can\'t find your school? Try typing the full school URL. + Tap here for login help. + + No Internet Connection + This action requires an internet connection. + A subject and description are required to submit feedback. + Version Number + Scroll to see all details + + You must enter a user id. + You must enter a valid domain. + Error trying to act as user. + \"Act as\" is essentially logging in as this user without a password. You will be able to take any action as if you were this user, and from other users\' points of views, it will be as if this user performed them. However, audit logs record that you were the one who performed the actions on behalf of this user. + You are acting as %s + Stop acting as… + You will stop acting as %s and return to your account. + We\'ve made a few changes. + See what\'s new + Find another school + diff --git a/libs/pandares/src/main/res/values-en/strings.xml b/libs/pandares/src/main/res/values-en/strings.xml new file mode 100644 index 0000000000..5929e49189 --- /dev/null +++ b/libs/pandares/src/main/res/values-en/strings.xml @@ -0,0 +1,1763 @@ + + + No Files Found + Search files + Enter a search term with three or more characters + + "Found no matching items" + "Found %d matching item" + "Found %d matching items" + + Flying panda in cape + Having trouble logging in? + Request Login Help + I\'m having trouble logging in + Please enter a valid email address + Sections: %s + Selected: %s + + + + + Assignment Details + Submission Types + No Content + Due + Successfully submitted! + Your submission is now waiting to be graded + Submission Uploading… + Tap to view progress + Submission Failed + Tap to view details + Submission & Rubric + Comments & Rubric + There was a problem loading this assignment. Please check your connection and try again. + Submitted + Not Submitted + Allowable File Types + Attempts + Attempts Allowed + Attempts Used + No attempts left + Resubmit Assignment + Launching External Tool… + Launch External Tool + Preview + One or more files failed to upload. Check your internet connection and retry to submit. + %1$s of %2$s + Submission Success! + Your assignment was successfully submitted. Enjoy your day! + Cancel Submission + This will cancel and delete your submission. + Submission Deleted + Missing + Graded + Reminder + Add due date reminder notifications about this assignment on this device. + Add reminder + Remove reminder + %s Before + + 1 Minute + %d Minutes + + + 1 Hour + %d Hours + + + 1 Day + %d Days + + + 1 Week + %d Weeks + + Custom + Custom Reminder + Quantity + Minutes Before + Hours Before + Days Before + Weeks Before + Reminder Notifications + Canvas Notifications for assignment reminders. + Due Date Reminder + This assignment is due in %s: %s + Please choose a future time for your reminder! + You have already set a reminder for this time + Delete Reminder + Are you sure you would like to delete this reminder? + You need to enable exact alarm permission for this action + + + No preview available for URLs using \'http://\' + Please enter a valid URL + Enter a URL here for your submission + Show Add File Options + Record Audio + Record Video + Text Submission Editor + + + Write… + Something went wrong on submission upload. Submit again. + + + Submission + Submission versions + Files (%d) + Rubric + Submission Error + This submission was a URL to an external page. We\'ve included a snapshot of what the page looked like when it was submitted. + No Submission Yet + Your assignment was locked on %1$s at %2$s + Your assignment will unlock on %1$s at %2$s + Your assignment is locked by module \"%1$s\" + Your assignment is locked by a module requirement + Assignment Locked + No Submission Allowed + Preview of the entered URL + Website URL + This assignment does not allow online submissions + This assignment does not allow online submissions + No Online Submissions + This assignment links to an external tool for submissions. + Open Tool + Submission text + + + Out of %s points + Out of %s pts + Excused + Final Grade: %s + %1$s/%2$s + %1$s out of %2$s points + Enter what-if score + Low: %s + Mean: %s + High: %s + + + Custom score + There is no rubric for this assignment + View long description + + + Uploading file %1$d of %2$d + Uploading comment for %s + Uploading media file + Comment upload failed for %s + Attach files to your comment by tapping an option below + Have questions about your assignment?\nMessage your instructor. + This message could not be sent. Tap to try again. + Media Upload + Media Upload - Audio + Media Upload - Video + Text Submission + External Tool Submission + Discussion Submission + Quiz Submission + Attempt %d + Media File + Audio + Video + + + Version: + + + Canvas Logo + Enter your Canvas URL: + Enter a value + myschool.instructure.com + Trying to sign into Canvas Network? + This app is not authorized for use + The server you entered is not authorized for this app. + The user agent for this app is unauthorized. + We were unable to verify the server for use with this app. + Okay + Options + + Attach a file + Edit + + + Remove this user? + You\'ll have to sign into this user again to access their content. + + Bookmarks + Add Bookmark + Label + A Label is Required + Bookmark Created + Bookmark could not be created + Select a Bookmark + Bookmark Removed + Remove Bookmark? + Edit Bookmark + Bookmark Updated + Create a bookmark then view it here! + + + Today + Past + No Date + Future + Next 7 Days + Due today at %s + Due tomorrow at %s + Due yesterday at %s + Your assignment has no due date + Due %1$s at %2$s + am + pm + + + + Locked + Assignments + There is no description for this assignment + No Assignments in this group + This assignment is excused and will not be considered in the total calculation + EX + Sort by + Time + Type + Sort by Time + Sort by Type + Cancel + All + Sort assignments button, sort by time + Sort assignments button, sort by type + Assignments sorted by time + Assignments sorted by type + + + Points\u0020 + Save + Cancel + + + Replies are only visible to those who have posted at least one reply. + This file is currently locked + Discussion Reply Editor + + + Starts + Ends + All Day Event + at + Personal Calendar + No Location Specified + + Discussions + Closed for Comments + Discussion Upload + Total: + N/A + Base on graded assignments + Show What-If Score + What-If Score + %1$s/%2$s (%3$s) + %1$s out of %2$s points, %3$s + Inbox + Unread + Archived + Sent + Grade + Clear filter + Add Message + Monologue + No items to display + To Do + Filter Courses + Filter by… + Favorited Courses + Event + Event was successfully deleted + Time Icon + Turn in + No Due Date + + Due at + Due at %s + Due + Due %1$s + + %d needs grading + %d need grading + + Create A Folder + Create Folder + Create A File + Show Create File and Create Folder Buttons + Hide Create File and Create Folder Buttons + An error occurred during folder creation. + Rename + Rename file + Rename folder + Name cannot be blank + Are you sure you want to delete the file \'%s\'? This action cannot be undone. + Are you sure you wish to delete the folder \'%s\'? This action cannot be undone. + + Are you sure you wish to delete the folder \'%1$s\', including the %2$d item it contains? This action cannot be undone. + Are you sure you wish to delete the folder \'%1$s\', including the %2$d items it contains? This action cannot be undone. + + + Settings + Name + Email + Username + Password + Authentication Required + Invalid email or password + Login ID + Domain + Send Feedback + Send Email… + Edit + + %d item + %d items + + Add to Home + Archive + Move to Inbox + Mark as Unread + Select People + Delete + Delete Event + Selected + An error occurred trying to send your message. Please try again. + That conversation has been deleted. + New avatar picture failed to upload + Edit Photo + That username is invalid. + Username successfully updated! + Take photo + Choose photo from Gallery + File not found. + + + Reply + Write a message… + Compose + Compose Message + Message was successfully sent. + The message cannot be blank! + No Recipients + Send + Users + Individually + Version: + v. %s + All Courses + All Groups + Course Options + Course options for %s + Edit nickname + Edit course color + Open + Open with alternate app + Opening File… + Download + Message Attachments + Attachment + Attachment Icon + OK + with + Star Conversation + Delete Conversation + Shared with \u0020 + Shared with you + Tap the \"+\" to create a new conversation. + All + Filter Inbox + Select a course or group + No messages + Remove attachment + Download Attachment + Message Options + Forward + Reply All + Unarchive + Are you sure you want to delete your copy of this message? This action cannot be undone. + Are you sure you want to delete your copy of this conversation? This action cannot be undone. + Unable to perform this action. Please check your connection and try again. + Conversation archived + Conversation unarchived + Message deleted + Send individual message to each recipient + You are not allowed to send messages to one or more of the selected recipients. + New Message + Forward Message + Add another recipient. Messages addressed only to yourself cannot be sent. + Select Recipients + + + %d person + %d people + + + + %d group + %d groups + + + Exit without saving? + Are you sure you would like to exit without saving? + Exit + + + Group Message? + Add everyone to a single group conversation, or message everyone individually? + Group + Groups + Group Members + + Loading… + Select from the list + + Syllabus + Summary + People + Teachers & TAs + Students + Observers + A syllabus not has been added. + There was an error loading your modules. + + Canvas + Choose Recipient(s) + This message currently has no recipients. + Send Message + User Avatar + + Assignment Icon + Announcement Icon + Conversation Icon + Default Icon + Discussion Icon + Grades Icon + Grades + All Grading Periods + Calendar + Bookmarks + Show Grades + Color Overlay + Grades are not visible for this course. + + This entire group has already been selected. + There are no users in this group + + + Deleted + Grade updated + + Loading Canvas Content… + + UnknownDevice + + The link selected is for a different domain than the one you are signed into. + + Pages + There is no page information available. + There are no pages available for this + Last Modified: + Last Modified: %1$s + + Start Acting As User + Stop Acting As User + User ID + There was an error when trying to act as user + + The ID cannot be blank + + Go To Quiz + + Quizzes + + Last post + + New Announcement + New Discussion + Create Discussion + Create Announcement + + Post Announcement + Post Discussion + + The announcement was posted successfully. + The discussion was posted successfully. + The discussion was updated successfully. + Discussion draft created successfully. + + There was an error posting the announcement. + There was an error posting the discussion. + Allow Threaded Replies + Users must post before seeing replies + Allow users to comment + Message + Title + + + The message cannot be blank. + + Sorry. You are not authorized to post announcements in this course. + Sorry. You are not authorized to post discussions in this course. + + Announcements + + The title cannot be blank. + + + Modules + View this item + You have viewed this item + Must submit assigment + Assignment submitted + Contribute to this page + You have contributed + Score at least a + Minimum score met + Prerequisites: + Unlocked: + Locked + This assignment is part of the module %s and hasn\'t been unlocked yet. + This page is part of the module %s and hasn\'t been unlocked yet. + This file is part of the module %s and hasn\'t been unlocked yet. + This quiz is part of the module %s and hasn\'t been unlocked yet. + This discussion is part of the module %s and hasn\'t been unlocked yet. + You must first complete: + It will be unlocked at: + Go To Modules + Mark done + Module Item Not Found + There was an error loading your modules. + + Help + Instructor Question + Link + + + This assignment is locked. + + + My Courses + + + + Text Entry + Online URL + This submission only accepts one file upload + Add Website Entry + Media Recordings + Submit + + + + Unsaved Progress + Unsaved information will be lost. Do you want to continue? + / + + + Confirm + + + Mark as done + + + Next + Add a comment… + + Home + Notifications + + Icon + Root User Folder + Root Course Folder + Root Group Folder + + + Privately available + Publicly available + Starts: \u0020 + Course Code: \u0020 + Ends: \u0020 + Visibility: \u0020 + License: \u0020 + + + + Collaborations + Conferences + Chat + Outcomes + + + Visit Page: + A snapshot of the website was taken when you turned it in. Tap and hold the image below to open or download the full image. + This submission was a URL to an external page. Keep in mind that this page may have changed since the submission originally occurred. + A preview of the submitted url + + + %1$s are not supported. + The link is not supported. + Open In Browser + Unsupported + + + + Profile + + (No Subject) + Subject + Sad Panda Image + Panda Fact:  + Remove + Founders + + EULA + Privacy Policy + Terms of Use + Canvas on GitHub + + + Assignment Quizzes + Practice Quizzes + Graded Surveys + Surveys + + + + To Do + Grades + Notifications + Canvas - To Do + Canvas - Grades + Canvas - Notifications + You are not logged in + Choose your widget style + Hide details on widget + Light + Dark + + + Ask Your Instructor a Question + Questions are submitted to your instructor + Search the Canvas Guides + Canvas Guides + Find answers to common questions + Report a Problem + If the app misbehaves, let us know + Request a Feature + Have an idea to improve the app? + Share Your Love for the App + Tell us about your favorite parts of the app + + + Idea for Canvas [Android] + The following information will help us better understand your idea: + + + Which course is this question about? + This message will be sent to all Teachers and TAs in the course + Sending… + Error + + + Courses + To Do List + Notifications + Push Notifications + Push Notifications have not been registered for this device. + Profile Settings + Account Preferences + PIN and Fingerprint + Pair with Observer + Have your parent scan this QR code from the Canvas Parent app to pair with you. This code will expire in seven days, or after one use. + Pairing Code:\u0020 + Pairing Code Error + Unable to retrieve a pairing code. This feature is only supported for students. + + Open Speedgrader + + todo to do todos todo list + course courses class classes + grade grades + + + Upload To + Upload To Canvas + My Files + Course Files + Upload to Submission Comment + + Changelog + + Edit Username + + Take a new photo + Choose from gallery + Set to default + Choose backdrop image + + Tap to add a course + No courses here, take the day off! + Please enroll in a course to see your grades + + Open navigation drawer + Close navigation drawer + + Landing Page + Could not find the course enrollment. + Could not find the group enrollment. + An unexpected error occurred. + This page is hidden or locked and cannot be accessed. + + Close + Closed + + Overdue Assignments + Upcoming Assignments + Undated Assignments + Past Assignments + + There was an error getting the course for this item. + There was an error getting the group for this item. + + Select a backdrop + + Submitting files… Check notification bar for updates. + Finished submitting file + + Bio + Draft + Publish + + Create Panda Avatar + Back + Set as avatar + The panda avatar was successfully saved + Avatar successfully saved. + There was an error saving the panda avatar + Panda Avatar + Panda Avatar Head + Panda Avatar Body + Panda Avatar Legs + PandaAvatars + Share + + SpeedGrader + Gauge + Grade slider + + Create New Event + Turn off the pandas + + Announcements` + Add Account + + Change User + + Please check your data connection and try again. + + + Canvas Notification + General Canvas Notifications + Further configuration of notifications can be done within the Canvas Notification Preferences section. + + Course Activities + Discussions + Conversations + Scheduling + Groups + Alerts + Conferences + + Due Date + Grading Policies + Course Content + Files + Announcement + Announcement Created By You + Grading + Invitation + All Submissions + Late Grading + Submission Comment + + Discussion + Discussion Post + + Add To Conversation + Conversation Message + Conversations Created By You + + Student Appointment Signups + Appointment Signups + Appointment Cancelations + Appointment Availability + Calendar + + Membership Update + Administrative Notifications + Recording Ready + + Email + Device + SMS + + Device Notifications + Would you like to turn on device notifications? These settings can be changed later in Settings > Notifications > For all Devices + Notifications have been enabled. + + Get notified when an assignment due date changes. + Get notified when course grading policies change. + Get notified when course content changes on WikiPages, Quizzes, and Assignments. + Get notified when a new file is added to your course. + Get notified when there is a new announcement in your course. + Get notified when you create an announcement and when somebody replies to your announcement. + Get notified when an assignment/submission was graded/changed and when a grade weight was changed. + Get notified for invitations to web conferences, groups, collaborations, peer reviews, and reminders. + Instructor & Admin only. Get notified when an assignment is submitted or resubmitted. + Instructor & Admin only. Get notified when a late assignment is submitted. + Get notified when a comment is made on your submission. + Get notified when there’s a new discussion topic in your course. + Get notified when there’s a new post in a discussion you’re subscribed to. + Get notified when you’re added to a conversation. + Get notified when you have a new inbox message. + Get notified when you create a new conversation. + Instructor & Admin only. Get notified when there’s an appointment signup. + Get notified when there’s a new signup on your calendar. + Get notified when there’s an appointment cancellation. + Get notified when an appointment slot becomes available. + Get notified about new and updated calendar items. + Admin only, pending enrollment activated. Get notified when a group enrollment is accepted or rejected. + Instructor & Admin only. Get notified about course enrollments, reports generated, content exported, migration reports, new account users, and new student groups. + Get notified when a conference recording is ready. + + Unlimited + + Question + + %d question + %d questions + + + + %s point + %s points + + + Time Limit + %1$s New Notifications + like + Like Entry + likes + + %s like + %s likes + + + Red + Hot Pink + Lavender + Violet + Purple + Slate + Blue + Cyan + Green + Chartreuse + Yellow + Gold + Orange + Pink + Gray + + Edit Course Nickname + The course nickname could not be set at this time. + + Course Color + Personalize your course by setting a new color. + The course color could not be set at this time. + + Pink + Hot Pink + Violet + Purple + Dark Blue + Blue + Cyan + Aqua Blue + Emerald Green + Green + Chartreuse + Yellow + Orange + Dark Orange + Red + + Course %s, favorite. + Course %s, not favorite. + Group %s, favorite. + Group %s, not favorite. + Account Group + + + From the course customization screen add a course or group to view them here. + + Toolbar close + Show in \"My Courses\" + About + Add a shortcut to your course + Course Nickname + + Functionality unavailable while offline + Edit Dashboard + Select which courses you would like to see on the Dashboard + Edit your course list + + Dashboard + Previous + Page %d of %d + Language + System Default + Restarting Canvas + Changing the language will require the app to restart, are you sure? + Your system default language is not guaranteed to be supported and will require a restart, are you sure? + Locked until \"%s\" is graded + Locked Icon + Choose Assignment Group + Choose Assignment Path + Select + Choice %d + Updating module information… + Pending review + %s pts + Total Points + %s points + Score + %s / %s pts + See All + Welcome! + Dismiss + Tap to view announcement + Accept + Decline + out of + + + There are no files associated with this course. + There are no files associated with this group. + Unsupported File Type + This assignment only allows certain file types: %s + + + B + KB + MB + GB + TB + + + Launch link in external browser + This LTI tool could not be loaded at this time. + URL Submission + + Recent Activity + Modules + Assignments + Syllabus + + Refresh widget + + Copy + You have been invited. + Invite accepted! + Invite declined. + Late penalty (-%s) + Late Penalty: -%s pts + Your Grade: %s pts + Final Grade + + 99+ + + Opens in webview + Not supported on this device + + Downloading + Download failed + Download successful + + Full due date details + Submissions + Attachment + Go Back + + + %s pt + %s pts + + + %s point + %s points + + To + + We are unable to find an external app to view this LTI tool. + + Comment Upload + + Front Page + Edit Page + Description + Page successfully updated. + An error occurred trying to save this page. Try again. + Page title must be set. + Page Details + Saving + Are you sure you wish to delete this event? + Conferences are not yet supported on mobile. + File preview image + An error occurred trying to load this PDF. + So sorry! This feature isn\'t allowed in student view. + Nothing To See Here + Unsupported Feature + + + Add Student + Input the student pairing code provided to you. + Pairing Code… + Pairing Failed. Make sure your pairing code is correct and within the time limit of use. + Complete + Incomplete + + + Post Settings + Post Grades + Hide Grades + + %d grade currently posted + %d grades currently posted + + + %d grade currently hidden + %d grades currently hidden + + Post to… + Specific Sections + Everyone + Grades will be made visible to all students + Graded + Grades will be made visible to students with graded submissions + All Hidden + All grades are currently hidden. + All Posted + All grades are currently posted. + Grades Posted + Grades Hidden + Failed to post grades + Failed to hide grades + Grade before posting + Grade after posting + Grade override + Current grade + Opens in Canvas Student + Student View + Rate on the Play Store + + + + Chosen head: %s + Chosen body: %s + Chosen feet: %s + Purple heart dress with pink trim + Green star dress with blue trim + Red star dress with orange trim + Blue blazer, red bow-tie, and grey pants + Orange button-up and jeans + Red button-up and brown pants + Yellow sun graphic t-shirt and grey pants + Teal basketball graphic t-shirt and purple pants + Blue android graphic t-shirt and green pants + White instructure graphic t-shirt and blue pants + Dark grey three-star t-shirt and teal pants + Burgundy wizard uniform with magic wand and black pants + Bare panda torso + Teardrop aviator sunglasses and lipstick + Makeup and purple hair bow + Fancy moustache + Green party glasses + Brown spectacles + Golden masquerade mask and lipstick + Teardrop aviator sunglasses + Freckled cheeks + Bespectacled, forehead-scarred wizard + Bare feet + Pink shoes with purple bows + Blue shoes with green bows + Red shoes with orange bows + Red shoes + Choose head + Choose body + Choose feet + + + 00:00:00 + %1$d hours, %2$d minutes, and %3$d seconds + %1$s out of %2$s + Replay + Stop + Start recording audio + Stop recording audio + Start recording video + Stop recording video + Close recording view + Delete recording + Video Comment Replay + There was an error attempting to view the replay. + There was an error attempting to submit your media comment. + + Add video comment + Add audio comment + An unexpected error occurred while trying to record audio. + An unexpected error occurred while trying to record video. + + Questions: + Time Limit: + Allowed Attempts: + Instructions + None + View Quiz + View Discussion + This assignment is locked by the module \"%1$s\". + Choose Media File + Unknown Author + Unknown Date + %s. minus + %s. + %s %s + %s %s, %s + You can open Submission details from here + Grade: %s + + + %s Minute + %s Minutes + + + + No Conferences + There are no conferences to display yet + There is no description for this conference + There was an error loading your conferences + Concluded %s at %s + Started %s at %s + Not Started + In Progress + Join + New Conferences + Concluded Conferences + Conference Details + Recordings + Conference in progress + + Criterion rating %s + %s, %s + more information + Create File and Folder buttons visible + Create File and Folder buttons invisible + Unselect All + Select All + All courses + All groups + Select courses for Dashboard or navigate to course details. + Select groups for Dashboard or navigate to course details. + Current enrollments + Past enrollments + Future enrollments + Added to Dashboard + Removed from Dashboard + All added to Dashboard + All removed from Dashboard + Inactive courses could not be added to Dashboard. + Edit Dashboard + No courses here + You have to be enrolled in a course to add it to the Dashboard. + Remove from dashboard + Remove all from dashboard + Add to dashboard + Add all to dashboard + + + Account + Homeroom + Schedule + Grades + Resources + Welcome, %1$s! + My Subjects + View Previous Announcements + Failed to load Homeroom + Failed to refresh Homeroom + Nothing Due Today + %1$s due today + %1$s missing + Welcome! + Your subjects show up here. + You currently have no subjects. + Requires app restart + Select + Select Grading Period + Current Grading Period + Failed to load grades + Failed to load grades for grading period + Failed to refresh grades + No grades to display + Not Graded + Change grading period + Grades not available + Homeroom + Homeroom View + Important Links + Student Applications + Staff Contact Info + Choose a Course + Important Links + Teacher + Teaching Assistant + Your resources show up here. + Failed to load resources + Failed to refresh resources + + Open a more accessible alternative view + + Recipients + Subject + Select a course, %s + + This student\'s responses are hidden because this assignment is anonymous. + + You\'ve marked it as done. + Cannot change the due date when due in a closed grading period. + Jump to Today + %1$s, %2$s + send message + + Something went wrong + Failed to load quiz + Failed to load submission + Student Annotation + Student Annotation + No due date + No due date + Missing + Don\'t save + Save draft? + Your changes will not be saved otherwise + Tap here to continue + Draft Available + An error occurred while loading submission + Tap to see full content + Tap to open in external app + Important Dates + No important dates + Important Dates + + Comment Library + No suggestions available + Comment + %s marked as done + Error occured, please try again. + Accept invite + Decline invite + No notifications to show + + Document Scanning + Color + Grayscale + Monochrome + Original + Your device does not have any applications installed that can open this link. + + Anonymous discussions are currently not supported on mobile. Open in browser to view discussion. + Open in browser + + Switch to list view + Switch to grid view + + Select app theme + App Theme + Light + Dark + Same as device + + Canvas is now available in dark theme + Choose app theme + Light theme + Dark theme + Same as device theme + Save + You can change it later in app settings + + Grab + File Upload + + + %s unread message + %s unread messages + + + + %s unread notification + %s unread notifications + + + No annotation selected + + Email Notifications + Immediately + Daily + Weekly + Never + Select frequency + Close progress dialog + %1$s of %2$s + Uploading to Files + One or more files failed to upload. Check your internet connection and retry to submit. + Uploading submission to \"%s\" + + Uploading Submission + Submission Successful + Submission Failed + + Uploading File + Uploading Files + + File Upload Successful + File Upload Failed + Cancel Submission + This will cancel and delete your submission. + File Upload Failed + Upload to Canvas for %s. + Upload to My Files + Submit assignment + Choose a course + Choose a course, selected course is: %s + Choose an assignment + Choose an assignment, selected assignment is: %s + %1$s, %2$s + Cancel Upload? + This will cancel your upload. + Filter assignments + Filter Assignments + Cancel + All + Late + Missing + Graded + Upcoming + Created by Student View + Error occurred. The topic may no longer be available. + Camera permission was permanently denied. Go to app settings to allow it. + Can’t unpublish assignment if there are student submissions. + Subscribe to Calendar Feed + You can sync your Canvas calendar to your Google Calendar account by clicking the Subscribe button in this dialog. Then, you’ll need to go to the Google Calendar app on your device and enable sync in the settings for the new calendar. + Subscribe + Switch to Light Mode + Switch to Dark Mode + Either you\'re a new user or the Acceptable Use Policy has changed since you last agreed to it. Please agree to the Acceptable Use Policy before you continue. + Acceptable Use Policy + Acceptable Use Policy + I agree to the Acceptable Use Policy. + Submit + Failed to submit terms acceptance. + + Attempt %d + Questions: %d + Time limit: %s + Allowed attempts: %s + Attempts used: %d + An unexpected error occurred. + + Delete + Archive + Unarchive + Mark as read + Mark as unread + Star + Unstar + + Failed to perform operation + %s deleted + %s archived + %s unarchived + %s marked as read + %s marked as unread + %s starred + %s unstarred + + + This conversation will be deleted from the Inbox on all your devices. This action cannot be undone. + These conversations will be deleted from the Inbox on all your devices. This action cannot be undone. + + Failed to load next page. Check your internet connection. + Failed to refresh conversations. Check your internet connection. + Inbox + Select Course or Group + Clear + Course filter: %s + select + Conversation selected. Selection mode activated. Navigate for actions. + Conversation selected + Conversation deselected + Exit selection mode + Selection mode deactivated. + Avatar of %s + Undo + App + Domain + Login ID + Email + Version + There was a problem reloading this assignment. Please check your connection and try again. + Instructure logo + Preferences + Offline Content + Synchronization + + + Offline Content + Manage Offline Content + Storage + %s of %s Used + Other Apps + Canvas Student + Remaining + All Courses + Sync + %d Selected + Select All + Deselect All + An error occurred while loading the content. + No Courses + Your courses will be listed here, and then you can make them available for offline usage. + No Course Content + The course content will be listed here, and then you can make them available for offline usage. + Discard Changes? + If you choose to discard, the changes will not be saved. + Discard + Sync Offline Content? + This will sync ~%s content. It may result in additional charges from your data provider, if you are not connected to a Wi-Fi network. + This will sync ~%s content only while you are connected to a Wi-Fi network. + Sync + Loading... + Hang tight, we\'re getting things ready for you. + Expand content + Collapse content + Enabling the Auto Content Sync will take care of downloading the selected content based on the below settings. The content synchronization will happen even if the application is not running. If the setting is switched off then no synchronization will happen. The already downloaded content will not be deleted. + Sync Frequency + Auto Content Sync + Specify the recurrence of the content synchronization. The system will download the selected content based on the frequency specified here. + Sync Content Over Wi-Fi Only + If this setting is enabled the content synchronization will only happen if the device connects to a Wi-Fi network, otherwise it will be postponed until a Wi-Fi network is available. + Synchronization + Daily + Weekly + Sync Frequency + Turn Off Content Sync Over Wi-fi Only? + If this setting is enabled the content synchronization will only happen if the device connects to a Wi-Fi network, otherwise it will be postponed until a Wi-Fi network is available. + Turn Off + Manual + Offline Mode + Not Available Offline + This content is not available in offline mode. + This content is not available in offline mode. If you want to change your settings open the Offline Content screen from the dashboard when network is available. + Offline + Sync Failed + Downloading %1$s of %2$s + Queued + Offline Content Sync Completed + Offline Content Sync Failed + Cancel Sync? + It will stop offline content sync. You can do it again later. + One or more items failed to sync. Please check your internet connection and retry syncing. + Download starting + Courses cannot be added to favorites offline. + All Courses + Courses + Groups + All Courses + Selecting courses for Dashboard can only be done online. You can navigate to offline course details. + Note + Success! Downloaded %1$s of %2$s + Syncing Offline Content + Dismiss notification + + %d course is syncing. + %d courses are syncing. + + This assignment is no longer available. + You are offline + You currently don\'t have any courses that are available offline. + Offline Content Sync Success + Offline Content Sync Failed + Offline Sync updates + Canvas notifications for offline sync updates. + + %d course has been synced. + %d courses have been synced. + + Additional course content + Failed to update Dashboard cards order + Publish all Modules and Items + Publish Modules only + Unpublish all Modules and Items + Module Options + Publish Module and all Items + Publish Module only + Unpublish Module and all Items + Module options for %s + Module item options for %s + Publish Module Item + Unpublish Module Item + Publish? + This will make only the module visible to students. + This will make the module and all items visible to students. + Unpublish? + This will make the module and all items invisible to students. + This will make only this item visible to students. + This will make only this item invisible to students. + This will make all modules and items visible to students. + This will make only the modules visible to students. + This will make all modules and items invisible to students. + Only available with link + Schedule availability + Item published + Item unpublished + Only Module published + Module and all Items published + Module and all Items unpublished + Only Modules published + All Modules and all Items published + All Modules and all Items unpublished + Inherit from Course + Course Members + Institution Members + Public + Edit Permissions + Update + Availability + Visibility + Available From + Available Until + From + Until + Date + Time + Clear From Date + Clear Until Date + This process could take a few minutes. You may close the modal or navigate away from the page during this process. + Note + All Modules + All Modules and Items + Selected Modules and Items + Selected Modules + Publishing + Unpublishing + Modules and items that have already been processed will not be reverted to their previous state when the process is discontinued. + Success! + Update failed + Update cancelled + Hidden + Scheduled + Published + Unpublished + + %.0f pt + %.0f pts + + Publish + Unpublish + Publish + Unpublish + Refresh + + Jump to Today + Switch to month view + Switch to week view + No Events Today! + It looks like a great day to rest, relax, and recharge. + To Do + %s To Do + Excused + Missing + Graded + Submitted + %s pts + Due %1$s at %2$s + %1$s at %2$s + %1$s from %2$s to %3$s + There was an error loading your calendar + Failed to refresh calendar + Add new calendar item + Calendars + User + Courses + Groups + Select the calendars you want to see, up to %d. + You can only select up to %d calendars. + Calendars + Failed to load filters + + To Do + Date + Description + Delete To Do? + This will permanently delete your To Do item. + There was an error deleting this To Do. Please check your connection and try again. + New To Do + Edit To Do + Add title + Date + Time + Calendar + Details + There was an error saving this To Do. Please check your connection and try again. + Select Calendar + + Retry + + Location + Address + Details + There was a problem loading this event. Please check your connection and try again. + Delete Event? + This will permanently delete your event. + There was an error deleting this event. Please check your connection and try again. + Previous month %s + Previous week starting on %s + Next month %s + Next week starting on %s + + %d event + %d events + + Confirm Deletion + This event + All events + This and all following events + Add To Do + Add Event + + New Event + Edit Event + Add title + Date + From + Start Time + To + End Time + Frequency + Calendar + Location + Address + Details + There was an error saving this Event. Please check your connection and try again. + Frequency + Does Not Repeat + Daily + Weekly on %s + First + Second + Third + Fourth + Last + Monthly on the %s %s + Annually on %s + Every Weekday (Monday to Friday) + Custom + Custom Frequency + Ends + Repeats on + On Date + After Occurrences + + Day + Days + + + Week + Weeks + + + Month + Months + + + Year + Years + + on day %d + on the %s %s + Number of Occurrences + How many times you would like to repeat? + Max 400 + Confirm Changes + Start time cannot be after end time. + End time cannot be before start time. + Select all + Deselect all + Title + Title + %s, selected calendar + %s selected + Open actions + Close actions + Actions open. Navigate forward for actions + Actions closed + + + Alerts + Calendar + Courses + Inbox + Help + Manage Students + Settings + Switch Users + Log Out + No Students + You are not observing any students. + No Grade + There was an error loading your student’s courses. + No Courses + Your student’s courses might not be published yet. + Not a parent? + We couldn\'t find any students associated with your account + Return to login + Are you a student or teacher? + One of our other apps might be a better fit. Tap one to visit the Play Store + STUDENT + Canvas Student + TEACHER + Canvas Teacher + diff --git a/libs/pandautils/src/main/res/values-en/strings.xml b/libs/pandautils/src/main/res/values-en/strings.xml new file mode 100644 index 0000000000..901683048b --- /dev/null +++ b/libs/pandautils/src/main/res/values-en/strings.xml @@ -0,0 +1,490 @@ + + + + + Choose Media + Take Video + No Camera + Take Photo + Choose Photo From Gallery + + Edit + + + Uploading to Canvas + Preparing upload + Loading Files… + Upload Successful + Network not available - please try again later. + "Upload in progress. Check notifications for upload status. + Uploading file… + An error occurred uploading your file. Please try again. + Submit File(s) + Upload File(s) + Upload + An error occurred sending your message. Please try again. + + Upload Notifications + Canvas notifications for ongoing uploads. + + + + Add File + + Camera + Gallery + Device + Scanner + Other + Domain + Files + File + File %s + Folder %s + Add Item + Assignment + Attached Files + Turn in + Course + Sending… + + + Star Rating One + Star Rating Two + Star Rating Three + Star Rating Four + Star Rating Five + + + You have currently exceeded your file storage quota. + An error occurred turning in your files. Please try again. + An error occurred loading your file. Please check your internet connection and try again. + Files uploaded successfully. + Files submitted successfully. + Message sent successfully. + Other apps can share files with Canvas! Look for the icon below in apps and tap on Canvas. + That will open this app. Then just select which course and assignment that you want to turn in the file to. + Share icon + Submit Assignment + Assignment : + Submitting assignment… + Conversations + Inbox + Divider + Legal + COMPLETE your Homework + Take an Exam + File Icon + Uploading %d of %d + %s has been locked. + File is now ready to download, please try again. + The file cannot be downloaded without granting permission. + Permission Granted, please try again. + Select a File + Uploading submission for %s + Submission failed for %s + Successfully submitted %s + + + An unexpected error occurred. + A server error has occurred. + You are unauthorized to access this. Either you don\'t have permissions or you don\'t have access yet (like your course hasn\'t started yet) + That file no longer exists. + Your session has expired. Please login to start a new session. + Uploading a file from this source could not be completed. + There was an error getting the photo. Please try again. + Add some text to send a message + + You can\'t upload files to the selected assignment. + Please select a course + Select a course + Select an assignment + Loading Assignments… + Please select an assignment + This course has no assignments that allow file uploads. + As a teacher in this course, you can\'t submit files + You haven\'t selected any files. + One or more files has a file extension that isn\'t allowed + The selected file type is not allowed. + Allowed extensions:  + + + Please check your data connection and try again. + Your device doesn\'t have any applications installed that can handle this file. + Cannot open + An unexpected error occurred while trying to open the file. + An unexpected error occurred while trying to download the file. + + Done + + + To use this feature, permission must first be granted. + No Connection + + + Your favorited courses & groups show up here. + Add Courses/Groups + No Discussions + Start a conversation with others. + Start a conversation with your students. + Inbox Zero! + You\'re all caught up! + No Starred Messages + Keep important messages in this section by tapping the star in each message. + Nothing Sent + You haven\'t sent any messages. + Nothing Archived + You haven\'t archived any messages. + No Quizzes + Sorry, no quizzes exist yet. + There are no published quizzes to display. + No Announcements + Nothing has been announced yet. + No Courses + It looks like there aren\'t any courses associated with this account. Visit the web to create a course today. + No Assignments + It looks like assignments haven\'t been created in this space. Time to go out and explore. + It looks like no assignments have been published yet. + No Files + There are no files for this submission + There are no files associated with this account. + This folder is empty + Not Supported + No Events Today! + It looks like a great day to rest and relax. + No Submission + This student doesn\'t have a submission yet. + Locked + This assignment is locked until %1$s at %2$s. + Choose a File + Attach a file to your submission by tapping an option below. + Select a file to upload by tapping an option below + No Notifications + There\'s nothing to be notified of yet. + Well Done! + Your to-do list is empty. Time to recharge. + No Syllabus + A syllabus has not been created yet. + No Bookmarks + Bookmarks are shortcuts for any screen in the app. Add them on the screen you\'d like to bookmark. + No Modules + It looks like there aren\'t any modules created yet. + No Pages + It looks like no pages have been created yet. + Modules have been disabled for this course. + Something Went Wrong + + + Yes + No + + Inbox + Unread + Starred + Sent + Archived + + Replies are only visible to those who have posted at least one reply. + + Like + Likes + This assignment is locked. + This deleted discussion entry has no replies. + Deleted + [Deleted by %1$s] + This discussion is part of the module %s and has not been unlocked yet. + Graded Discussion + Points Possible + In this type of discussion, you cannot comment on any entry on this level. + + Overflow + + Comment + Comments + Unread + Reply + Close + + No Internet Connection + This action requires an internet connection. + Avatar + %s Avatar + View user details for %s. + + + Canvas + Canvas Teacher + Canvas Parent + Speedgrader + + + How are we doing? + How would you rate us? + What can we do better? + Send Feedback + Suggestions for Android + Domain + Version: + User ID + Email + Don\'t show again + Install Date: + Device + OS Version + Loading + Save + Pinch and drag to adjust the image. + + + + %s Reply + %s Replies + + + Deleted this reply %s + Unknown Author + Reply + Edit + Are you sure you want to delete this reply? + Available to: + Available from: + Due + Due: + Delete + Due %1s at %2s + -- + Availability: + Icon for pinned discussion + Pin Discussion + Unpin Discussion + Discussion Pinned + Pinned + Discussions + There are no discussions to show in this section. + Description + Allow threaded replies + Students must post before seeing replies + Closed for Comments + Open for Comments + Pin + Unpin + Options + Delete Discussion? + Delete Discussion + This will delete the whole discussion and thread. + The discussion reply was successful. + The discussion edit was successful. + The discussion reply cannot be empty. + The discussion reply was unable to be sent at this time. + New Discussion + Edit Discussion + Discussion Details + Discussion successfully updated. + Saving + Discussion successfully created. + The attachment could not be added to this discussion. Please check your file quota and try again. + No Title + Replies + More options + %s %s %s + Last post %s + Posted on %s + Replies are only visible to those who have posted at least one reply. + Delay Posting + Post At + Post Date + Post Time + Edit Announcement + New Announcement + Announcement Details + A description is required + An error occurred trying to save this announcement. Try again. + Announcement successfully updated. + Announcement successfully created. + + + %s Unread + %s Unread + + + + Add Attachment + Add Attachment + Add Attachments + + + + + Unlock date cannot be after due date + Lock date cannot be before due date + Lock date cannot be before unlock date + Assignee cannot be blank + Assign To + Available From + Available To + Available From Date + Available From Time + Available To Date + Available To Time + Due Date + Due Time + Add New + + + Download Attachment + Remove attachment + + + Continue without saving? + Are you sure you would like to continue without saving? + Continue + Exit without saving? + Are you sure you would like to exit without saving? + Exit + Cancel + + File icon + Turn in + Upload + Okay + Loading Files… + Allowed extensions:  + The selected file type is not allowed. + An error occurred uploading your file. Please try again. + Files uploaded successfully. + Uploading files… + Uploading file… + Your device does not have any applications installed that can handle this file. + There was an error getting the photo. Please try again. + One or more files has a file extension that is not allowed + You cannot upload files to the selected assignment. + You have not selected any files. + Attachments + Upload To + Upload To Canvas + My Files + Course Files + Group Files + Upload to Submission Comment + Attach a file + with + + Canvas Notification + General Canvas Notifications + Further configuration of notifications can be done within the Canvas Notification Preferences section. + + + %d new notifications + + + Copy link address + Copy Link + Link copied + Link + File Exists + Replace + Share link + + Launch External Tool + External Tool + External Tool + Visit your Canvas web account to edit this external tool. + We are unable to find an external app to view this LTI tool. + + This file could not be displayed. Use the button below to open the file with another app on your device. + Open with… + This media format is not supported + fullscreen + + Data Usage Warning + This action might consume a large amount of data, potentially incurring overage charges. Would you like to continue? + Do not show this message again + + Please check your data connection and try again. + Play media + Try again + Image Uploading... + Image Upload Error + Retry + Search + Search Assignments + Search Announcements + Search Discussions + Search Pages + Search Quizzes + No items match \"%s\" + + Selected + + App update notifications + Canvas notifications for in-app updates. + App ready to update + Restart the app to install the new version + Nothing planned yet + Graded + + %d Reply + %d Replies> + + Feedback + Late + Redo + Excused + Tomorrow + Yesterday + At %s + %1$s to %2$s + To Do: %s + Due: %s + Show %d missing items + Hide %d missing items + Unable to fetch your schedule + To Do + + %s pt + %s pt + %s pts + + + %.1f points + %.1f point + %.1f points + + All Day + Missing + Previous Week + Next Week + %1$s, %2$s + Course %s + Announcement + Discussion + Calendar event + Assignment + Planner note + Quiz + To Do + Page + Assessment request + Marked as done + Not marked as done + Uh oh! An error occurred while loading the course details. + %s marked as not done + %s Mark as done + %s Mark as not done + %s + From 1905700a82ef567c5bbf07470ed027f644ffa4f4 Mon Sep 17 00:00:00 2001 From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com> Date: Mon, 22 Jul 2024 17:25:52 +0200 Subject: [PATCH 08/50] [MBL-17710][Student] Fixed 'File upload dialog remembers index of deleted file' refs: MBL-17710 affects: Student release note: none --- .../PickerSubmissionUploadEffectHandler.kt | 7 ++++ .../picker/PickerSubmissionUploadModels.kt | 1 + .../picker/PickerSubmissionUploadUpdate.kt | 3 +- ...PickerSubmissionUploadEffectHandlerTest.kt | 36 +++++++++++++++++-- .../PickerSubmissionUploadUpdateTest.kt | 12 +++++-- 5 files changed, 52 insertions(+), 7 deletions(-) diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadEffectHandler.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadEffectHandler.kt index db123b31e8..f99647fdac 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadEffectHandler.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadEffectHandler.kt @@ -126,6 +126,9 @@ class PickerSubmissionUploadEffectHandler( is PickerSubmissionUploadEffect.HandleSubmit -> { handleSubmit(effect.model) } + is PickerSubmissionUploadEffect.RemoveTempFile -> { + removeTempFile(effect.path) + } }.exhaustive } @@ -289,6 +292,10 @@ class PickerSubmissionUploadEffectHandler( return false } + private fun removeTempFile(path: String) { + FileUploadUtils.deleteTempFile(path) + } + companion object { const val REQUEST_CAMERA_PIC = 5100 const val REQUEST_PICK_IMAGE_GALLERY = 5101 diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadModels.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadModels.kt index 9ba138616d..0a2ca09bfa 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadModels.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadModels.kt @@ -39,6 +39,7 @@ sealed class PickerSubmissionUploadEffect { data class HandleSubmit(val model: PickerSubmissionUploadModel) : PickerSubmissionUploadEffect() data class LoadFileContents(val uri: Uri, val allowedExtensions: List) : PickerSubmissionUploadEffect() + data class RemoveTempFile(val path: String) : PickerSubmissionUploadEffect() } data class PickerSubmissionUploadModel( diff --git a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadUpdate.kt b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadUpdate.kt index b53b07e2e7..00b68bcedb 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadUpdate.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/assignmentDetails/submission/picker/PickerSubmissionUploadUpdate.kt @@ -46,8 +46,9 @@ class PickerSubmissionUploadUpdate : } is PickerSubmissionUploadEvent.OnFileRemoved -> { val files = model.files.toMutableList() + val tempFilePath = files[event.fileIndex].fullPath files.removeAt(event.fileIndex) - Next.next(model.copy(files = files.toList())) + Next.next(model.copy(files = files.toList()), setOf(PickerSubmissionUploadEffect.RemoveTempFile(tempFilePath))) } is PickerSubmissionUploadEvent.OnFileAdded -> { val files = model.files.toMutableList() diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/PickerSubmissionUploadEffectHandlerTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/PickerSubmissionUploadEffectHandlerTest.kt index a6afdfac2f..c84e792211 100644 --- a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/PickerSubmissionUploadEffectHandlerTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/PickerSubmissionUploadEffectHandlerTest.kt @@ -23,15 +23,33 @@ import androidx.core.content.FileProvider import androidx.fragment.app.FragmentActivity import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.postmodels.FileSubmitObject -import com.instructure.pandautils.utils.* +import com.instructure.pandautils.utils.ActivityResult +import com.instructure.pandautils.utils.FilePrefs +import com.instructure.pandautils.utils.FileUploadUtils +import com.instructure.pandautils.utils.OnActivityResults +import com.instructure.pandautils.utils.PermissionUtils +import com.instructure.pandautils.utils.requestPermissions import com.instructure.student.R import com.instructure.student.mobius.assignmentDetails.isIntentAvailable -import com.instructure.student.mobius.assignmentDetails.submission.picker.* +import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionMode +import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionUploadEffect +import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionUploadEffectHandler +import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionUploadEvent +import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionUploadModel import com.instructure.student.mobius.assignmentDetails.submission.picker.ui.PickerSubmissionUploadView import com.instructure.student.mobius.common.ui.SubmissionHelper import com.instructure.student.mobius.common.ui.SubmissionService import com.spotify.mobius.functions.Consumer -import io.mockk.* +import io.mockk.confirmVerified +import io.mockk.every +import io.mockk.invoke +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.mockkStatic +import io.mockk.slot +import io.mockk.unmockkObject +import io.mockk.unmockkStatic +import io.mockk.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.asCoroutineDispatcher @@ -554,4 +572,16 @@ class PickerSubmissionUploadEffectHandlerTest : Assert() { fun `isPickerRequest with invalid code return false`() { assertFalse(PickerSubmissionUploadEffectHandler.isPickerRequest(1)) } + + @Test + fun `RemoveTempFile removes temp file`() { + mockkObject(FileUploadUtils) + connection.accept(PickerSubmissionUploadEffect.RemoveTempFile("path")) + + verify { + FileUploadUtils.deleteTempFile("path") + } + + unmockkObject(FileUploadUtils) + } } diff --git a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/PickerSubmissionUploadUpdateTest.kt b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/PickerSubmissionUploadUpdateTest.kt index bdfa8fc35b..08811ab839 100644 --- a/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/PickerSubmissionUploadUpdateTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/assignment/details/submission/PickerSubmissionUploadUpdateTest.kt @@ -19,7 +19,11 @@ package com.instructure.student.test.assignment.details.submission import android.net.Uri import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.postmodels.FileSubmitObject -import com.instructure.student.mobius.assignmentDetails.submission.picker.* +import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionMode +import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionUploadEffect +import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionUploadEvent +import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionUploadModel +import com.instructure.student.mobius.assignmentDetails.submission.picker.PickerSubmissionUploadUpdate import com.instructure.student.test.util.matchesEffects import com.spotify.mobius.test.FirstMatchers import com.spotify.mobius.test.InitSpec @@ -197,7 +201,7 @@ class PickerSubmissionUploadUpdateTest : Assert() { } @Test - fun `OnFileRemoved event results in model change to files and no effects`() { + fun `OnFileRemoved event results in model change to files and RemoveTempFile effect`() { val startModel = initModel.copy(files = listOf(initFile)) val expectedModel = startModel.copy(files = emptyList()) @@ -207,7 +211,9 @@ class PickerSubmissionUploadUpdateTest : Assert() { .then( assertThatNext( NextMatchers.hasModel(expectedModel), - NextMatchers.hasNoEffects() + matchesEffects( + PickerSubmissionUploadEffect.RemoveTempFile(initFile.fullPath) + ) ) ) } From 8b3878c0bcb754c1d225464a93aeb234e316b380 Mon Sep 17 00:00:00 2001 From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com> Date: Mon, 22 Jul 2024 17:26:25 +0200 Subject: [PATCH 09/50] [MBL-17709][Student] Fixed alarm permission error refs: MBL-17709 affects: Student release note: none --- .../features/assignments/details/AssignmentDetailsFragment.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsFragment.kt index ca4608a833..44ef515039 100644 --- a/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsFragment.kt @@ -456,6 +456,7 @@ class AssignmentDetailsFragment : ParentFragment(), Bookmarkable { } else { Snackbar.make(requireView(), getString(R.string.reminderPermissionNotGrantedError), Snackbar.LENGTH_LONG).show() } + viewModel.checkingReminderPermission = false } } From da1be54462fa51da847612f0343e29298a63f36f Mon Sep 17 00:00:00 2001 From: domonkosadam <53480952+domonkosadam@users.noreply.github.com> Date: Tue, 23 Jul 2024 16:55:40 +0200 Subject: [PATCH 10/50] [MBL-17610][All] Implement help dialog refs: MBL-17610 affects: All release note: none --- .../features/dashboard/DashboardFragment.kt | 3 +- .../help/ParentHelpDialogFragmentBehavior.kt | 21 +++--- .../features/help/ParentHelpLinkFilter.kt | 2 +- .../parentapp/util/navigation/Navigation.kt | 4 - .../main/res/layout/fragment_dashboard.xml | 3 +- .../features/help/ParentHelpLinkFilterTest.kt | 64 ++++++++++++++++ .../student/activity/NavigationActivity.kt | 13 +--- .../help/StudentHelpDialogFragmentBehavior.kt | 16 ++-- .../teacher/activities/InitActivity.kt | 23 +----- .../help/TeacherHelpDialogFragmentBehavior.kt | 20 ++--- .../activities/BaseLoginFindSchoolActivity.kt | 10 +-- .../BaseLoginLandingPageActivity.kt | 11 +-- .../login/dialog/ErrorReportDialog.kt | 75 ++++++++++++------- 13 files changed, 150 insertions(+), 115 deletions(-) create mode 100644 apps/parent/src/test/java/com/instructure/parentapp/features/help/ParentHelpLinkFilterTest.kt diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardFragment.kt index 7f6cd97522..95f65ab2c2 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardFragment.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardFragment.kt @@ -41,6 +41,7 @@ import androidx.navigation.NavController.Companion.KEY_DEEP_LINK_INTENT import androidx.navigation.fragment.NavHostFragment import com.instructure.canvasapi2.models.User import com.instructure.loginapi.login.tasks.LogoutTask +import com.instructure.pandautils.features.help.HelpDialogFragment import com.instructure.pandautils.interfaces.NavigationCallbacks import com.instructure.pandautils.utils.ColorKeeper import com.instructure.pandautils.utils.ViewStyler @@ -179,7 +180,7 @@ class DashboardFragment : Fragment(), NavigationCallbacks { R.id.inbox -> menuItemSelected { navigation.navigate(activity, navigation.inbox) } R.id.manage_students -> menuItemSelected { navigation.navigate(activity, navigation.manageStudents) } R.id.settings -> menuItemSelected { navigation.navigate(activity, navigation.settings) } - R.id.help -> menuItemSelected { navigation.navigate(activity, navigation.help) } + R.id.help -> menuItemSelected { activity?.let { HelpDialogFragment.show(it) } } R.id.log_out -> menuItemSelected { onLogout() } R.id.switch_users -> menuItemSelected { onSwitchUsers() } else -> false diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/help/ParentHelpDialogFragmentBehavior.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/help/ParentHelpDialogFragmentBehavior.kt index 560bf103a8..60c6f93c4b 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/help/ParentHelpDialogFragmentBehavior.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/help/ParentHelpDialogFragmentBehavior.kt @@ -17,6 +17,8 @@ package com.instructure.parentapp.features.help +import android.content.Intent +import androidx.core.net.toUri import androidx.fragment.app.FragmentActivity import com.instructure.loginapi.login.dialog.ErrorReportDialog import com.instructure.pandautils.features.help.HelpDialogFragmentBehavior @@ -24,23 +26,22 @@ import com.instructure.pandautils.utils.AppType import com.instructure.pandautils.utils.Utils import com.instructure.parentapp.R -class ParentHelpDialogFragmentBehavior(private val activity: FragmentActivity) : HelpDialogFragmentBehavior { - +class ParentHelpDialogFragmentBehavior(private val parentActivity: FragmentActivity) : HelpDialogFragmentBehavior { override fun reportProblem() { - val dialog = ErrorReportDialog() - dialog.arguments = ErrorReportDialog.createBundle(activity.getString(R.string.appUserTypeStudent)) - dialog.show(activity.supportFragmentManager, ErrorReportDialog.TAG) + ErrorReportDialog().apply { + arguments = ErrorReportDialog.createBundle(parentActivity.getString(R.string.appUserTypeParent)) + show(parentActivity.supportFragmentManager, ErrorReportDialog.TAG) + } } override fun rateTheApp() { - Utils.goToAppStore(AppType.PARENT, activity) + Utils.goToAppStore(AppType.PARENT, parentActivity) } - override fun askInstructor() { - // TODO: Implement - } + override fun askInstructor() = Unit override fun openWebView(url: String, title: String) { - // TODO: Implement + val intent = Intent(Intent.ACTION_VIEW, url.toUri()) + parentActivity.startActivity(intent) } } diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/help/ParentHelpLinkFilter.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/help/ParentHelpLinkFilter.kt index 88ab4bf00f..6078689d42 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/help/ParentHelpLinkFilter.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/help/ParentHelpLinkFilter.kt @@ -24,6 +24,6 @@ import com.instructure.pandautils.features.help.HelpLinkFilter class ParentHelpLinkFilter : HelpLinkFilter { override fun isLinkAllowed(link: HelpLink, favoriteCourses: List): Boolean { - return link.availableTo.contains("parent") || link.availableTo.contains("user") + return link.availableTo.contains("observer") || link.availableTo.contains("user") } } diff --git a/apps/parent/src/main/java/com/instructure/parentapp/util/navigation/Navigation.kt b/apps/parent/src/main/java/com/instructure/parentapp/util/navigation/Navigation.kt index 4e51d14a65..46dd219f5f 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/util/navigation/Navigation.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/util/navigation/Navigation.kt @@ -7,10 +7,8 @@ import androidx.navigation.NavGraph import androidx.navigation.NavType import androidx.navigation.createGraph import androidx.navigation.findNavController -import androidx.navigation.fragment.dialog import androidx.navigation.fragment.fragment import com.instructure.canvasapi2.utils.ApiPrefs -import com.instructure.pandautils.features.help.HelpDialogFragment import com.instructure.pandautils.features.inbox.list.InboxFragment import com.instructure.parentapp.R import com.instructure.parentapp.features.alerts.list.AlertsFragment @@ -37,7 +35,6 @@ class Navigation(apiPrefs: ApiPrefs) { val calendar = "$baseUrl/calendar" val alerts = "$baseUrl/alerts" val inbox = "$baseUrl/conversations" - val help = "$baseUrl/help" val manageStudents = "$baseUrl/manage-students" val settings = "$baseUrl/settings" @@ -71,7 +68,6 @@ class Navigation(apiPrefs: ApiPrefs) { } fragment(manageStudents) fragment(settings) - dialog(help) fragment(courseDetails) { argument(courseId) { type = NavType.LongType diff --git a/apps/parent/src/main/res/layout/fragment_dashboard.xml b/apps/parent/src/main/res/layout/fragment_dashboard.xml index 41fd89a64d..2c7ae87d14 100644 --- a/apps/parent/src/main/res/layout/fragment_dashboard.xml +++ b/apps/parent/src/main/res/layout/fragment_dashboard.xml @@ -19,6 +19,7 @@ + { + return listOf( + HelpLink( + id = "1", + type = "type", + availableTo = listOf("observer", "user"), + url = "url", + text = "text", + subtext = "subtext" + ), + HelpLink( + id = "2", + type = "type", + availableTo = listOf("user"), + url = "url", + text = "text", + subtext = "subtext" + ), + HelpLink( + id = "3", + type = "type", + availableTo = listOf("observer"), + url = "url", + text = "text", + subtext = "subtext" + ), + HelpLink( + id = "4", + type = "type", + availableTo = listOf("other"), + url = "url", + text = "text", + subtext = "subtext" + ) + ) + } +} \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt b/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt index 0da611c2bf..bc8c995538 100644 --- a/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt +++ b/apps/student/src/main/java/com/instructure/student/activity/NavigationActivity.kt @@ -74,7 +74,6 @@ import com.instructure.interactions.Navigation import com.instructure.interactions.router.Route import com.instructure.interactions.router.RouteContext import com.instructure.interactions.router.RouterParams -import com.instructure.loginapi.login.dialog.ErrorReportDialog import com.instructure.loginapi.login.dialog.MasqueradingDialog import com.instructure.loginapi.login.tasks.LogoutTask import com.instructure.pandautils.binding.viewBinding @@ -167,8 +166,7 @@ private const val BOTTOM_SCREENS_BUNDLE_KEY = "bottomScreens" @AndroidEntryPoint @Suppress("DELEGATED_MEMBER_HIDES_SUPERTYPE_OVERRIDE") class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog.OnMasqueradingSet, - FullScreenInteractions, ActivityCompat.OnRequestPermissionsResultCallback by PermissionReceiver(), - ErrorReportDialog.ErrorReportDialogResultListener { + FullScreenInteractions, ActivityCompat.OnRequestPermissionsResultCallback by PermissionReceiver() { private val binding by viewBinding(ActivityNavigationBinding::inflate) private lateinit var navigationDrawerBinding: NavigationDrawerBinding @@ -1230,15 +1228,6 @@ class NavigationActivity : BaseRouterActivity(), Navigation, MasqueradingDialog. } } - override fun onTicketPost() { - // The message is a little longer than normal, so show it for LENGTH_LONG instead of LENGTH_SHORT - Toast.makeText(this, R.string.errorReportThankyou, Toast.LENGTH_LONG).show() - } - - override fun onTicketError() { - toast(R.string.errorOccurred) - } - private fun createBottomNavFragment(name: String?): Fragment? { return when (name) { navigationBehavior.homeFragmentClass.name -> { diff --git a/apps/student/src/main/java/com/instructure/student/mobius/settings/help/StudentHelpDialogFragmentBehavior.kt b/apps/student/src/main/java/com/instructure/student/mobius/settings/help/StudentHelpDialogFragmentBehavior.kt index b9aea5dad9..674e73f3d0 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/settings/help/StudentHelpDialogFragmentBehavior.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/settings/help/StudentHelpDialogFragmentBehavior.kt @@ -25,23 +25,23 @@ import com.instructure.student.R import com.instructure.student.activity.InternalWebViewActivity import com.instructure.student.dialog.AskInstructorDialogStyled -class StudentHelpDialogFragmentBehavior(private val activity: FragmentActivity) : HelpDialogFragmentBehavior { - +class StudentHelpDialogFragmentBehavior(private val parentActivity: FragmentActivity) : HelpDialogFragmentBehavior { override fun reportProblem() { - val dialog = ErrorReportDialog() - dialog.arguments = ErrorReportDialog.createBundle(activity.getString(R.string.appUserTypeStudent)) - dialog.show(activity.supportFragmentManager, ErrorReportDialog.TAG) + ErrorReportDialog().apply { + arguments = ErrorReportDialog.createBundle(parentActivity.getString(R.string.appUserTypeStudent)) + show(parentActivity.supportFragmentManager, ErrorReportDialog.TAG) + } } override fun rateTheApp() { - Utils.goToAppStore(AppType.STUDENT, activity) + Utils.goToAppStore(AppType.STUDENT, parentActivity) } override fun askInstructor() { - AskInstructorDialogStyled().show(activity.supportFragmentManager, AskInstructorDialogStyled.TAG) + AskInstructorDialogStyled().show(parentActivity.supportFragmentManager, AskInstructorDialogStyled.TAG) } override fun openWebView(url: String, title: String) { - activity.startActivity(InternalWebViewActivity.createIntent(activity, url, title, false)) + parentActivity.startActivity(InternalWebViewActivity.createIntent(parentActivity, url, title, false)) } } \ No newline at end of file diff --git a/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt b/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt index 4c09b7c876..406a76e0be 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/activities/InitActivity.kt @@ -102,7 +102,7 @@ import javax.inject.Inject @AndroidEntryPoint class InitActivity : BasePresenterActivity(), InitActivityView, DashboardFragment.CourseBrowserCallback, InitActivityInteractions, - MasqueradingDialog.OnMasqueradingSet, ErrorReportDialog.ErrorReportDialogResultListener, OnUnreadCountInvalidated { + MasqueradingDialog.OnMasqueradingSet, OnUnreadCountInvalidated { private val binding by viewBinding(ActivityInitBinding::inflate) private lateinit var navigationDrawerBinding: NavigationDrawerBinding @@ -667,27 +667,6 @@ class InitActivity : BasePresenterActivity get() = (binding.severitySpinner.selectedItem as? Pair) ?: severityOptions[0] - interface ErrorReportDialogResultListener { - fun onTicketPost() - fun onTicketError() - } - - override fun onAttach(context: Context) { - super.onAttach(context) - try { - resultListener = context as ErrorReportDialogResultListener - } catch (e: ClassCastException) { - throw ClassCastException("${context::class.java.name} must implement ErrorReportDialogResultListener") - } - } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.dialog_error_report, container, false) } @@ -227,18 +221,43 @@ class ErrorReportDialog : DialogFragment() { severity = severity ) + cancelButton.setInvisible() + sendButton.setInvisible() + progressBar.setVisible() + try { - cancelButton.setInvisible() - sendButton.setInvisible() - progressBar.setVisible() awaitApi { ErrorReportManager.postErrorReport(report, useDefaultDomain, it) } - resultListener?.onTicketPost() - dismiss() + + onTicketPost() } catch (e: Throwable) { - resultListener?.onTicketError() - cancelButton.setVisible() - sendButton.setVisible() - progressBar.setGone() + onTicketError() + } + } + } + + private fun onTicketPost() { + dismiss() + dismissHelpDialog() + Toast.makeText(activity, R.string.errorReportThankyou, Toast.LENGTH_LONG).show() + } + + private fun onTicketError() = with(binding) { + cancelButton.setVisible() + sendButton.setVisible() + progressBar.setGone() + + dismiss() + dismissHelpDialog() + Toast.makeText(activity, R.string.errorOccurred, Toast.LENGTH_LONG).show() + } + + private fun dismissHelpDialog() { + val fragment = activity?.supportFragmentManager?.findFragmentByTag(HelpDialogFragment.TAG) + if (fragment is HelpDialogFragment) { + try { + fragment.dismiss() + } catch (e: IllegalStateException) { + Logger.e("Committing a transaction after activities saved state was called: " + e) } } } From 19fa7c62aad595a653a35ea2a532229830e6e899 Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Wed, 24 Jul 2024 10:08:09 +0200 Subject: [PATCH 11/50] [MBL-17765][Student] Fallback destructive migration for student db Test plan: See ticket. refs: MBL-17765 affects: Student release note: none --- .../src/main/java/com/instructure/student/di/DatabaseModule.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/student/src/main/java/com/instructure/student/di/DatabaseModule.kt b/apps/student/src/main/java/com/instructure/student/di/DatabaseModule.kt index c633c6e42e..0455a574bc 100644 --- a/apps/student/src/main/java/com/instructure/student/di/DatabaseModule.kt +++ b/apps/student/src/main/java/com/instructure/student/di/DatabaseModule.kt @@ -42,6 +42,7 @@ class DatabaseModule { fun provideStudentDb(@ApplicationContext context: Context): StudentDb { return Room.databaseBuilder(context, StudentDb::class.java, "student.db") .addMigrations(*studentDbMigrations) + .fallbackToDestructiveMigration() .build() } } \ No newline at end of file From 109933a1f83737ef215568860b760c6ee320aac1 Mon Sep 17 00:00:00 2001 From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:12:44 +0200 Subject: [PATCH 12/50] [MBL-17764][Student] Submission details crashes #2503 refs: MBL-17764 affects: Student release note: Fixed a crash on the submission details screen. --- .../com/instructure/student/mobius/common/ui/MobiusFragment.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/student/src/main/java/com/instructure/student/mobius/common/ui/MobiusFragment.kt b/apps/student/src/main/java/com/instructure/student/mobius/common/ui/MobiusFragment.kt index bd4b8b96b5..6b56a89e2c 100644 --- a/apps/student/src/main/java/com/instructure/student/mobius/common/ui/MobiusFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/mobius/common/ui/MobiusFragment.kt @@ -103,7 +103,6 @@ abstract class MobiusFragment Date: Thu, 25 Jul 2024 13:16:54 +0200 Subject: [PATCH 13/50] [MBL-17736][Student] Hide Studio submission type if video file submission is not allowed (#2505) Test plan: See ticket. refs: MBL-17736 affects: Student release note: Fixed a bug, where Studio submission was allowed incorrectly. --- .../details/AssignmentDetailsFragment.kt | 2 +- .../details/AssignmentDetailsViewModel.kt | 11 + .../AssignmentDetailsViewModelTest.kt | 235 ++++++++++++------ .../pandautils/utils/FileExtensions.kt | 6 + 4 files changed, 180 insertions(+), 74 deletions(-) diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsFragment.kt index 44ef515039..e87be8aa88 100644 --- a/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsFragment.kt @@ -323,7 +323,7 @@ class AssignmentDetailsFragment : ParentFragment(), Bookmarkable { setupDialogRow( dialog, dialogBinding.submissionEntryStudio, - (submissionTypes.contains(SubmissionType.ONLINE_UPLOAD) && assignment.isStudioEnabled) + viewModel.isStudioAccepted() ) { navigateToStudioScreen(assignment, studioLTITool) } diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsViewModel.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsViewModel.kt index 4e6f5c0c59..7ff7b258c4 100644 --- a/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsViewModel.kt +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/details/AssignmentDetailsViewModel.kt @@ -56,6 +56,7 @@ import com.instructure.pandautils.utils.AssignmentUtils2 import com.instructure.pandautils.utils.ColorKeeper import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.HtmlContentFormatter +import com.instructure.pandautils.utils.isAudioVisualExtension import com.instructure.pandautils.utils.orDefault import com.instructure.student.R import com.instructure.student.features.assignments.details.gradecellview.GradeCellViewData @@ -675,4 +676,14 @@ class AssignmentDetailsViewModel @Inject constructor( val dueDate = assignment?.dueDate?.time ?: return null return dueDate - reminderChoice.getTimeInMillis() } + + fun isStudioAccepted(): Boolean { + if (assignment?.isStudioEnabled == false) return false + + if (assignment?.getSubmissionTypes()?.contains(SubmissionType.ONLINE_UPLOAD) == false) return false + + if (assignment?.allowedExtensions?.isEmpty() == true) return true + + return assignment?.allowedExtensions?.any { isAudioVisualExtension(it) } ?: true + } } diff --git a/apps/student/src/test/java/com/instructure/student/features/assignmentdetails/AssignmentDetailsViewModelTest.kt b/apps/student/src/test/java/com/instructure/student/features/assignmentdetails/AssignmentDetailsViewModelTest.kt index e18c933005..914f2e5f31 100644 --- a/apps/student/src/test/java/com/instructure/student/features/assignmentdetails/AssignmentDetailsViewModelTest.kt +++ b/apps/student/src/test/java/com/instructure/student/features/assignmentdetails/AssignmentDetailsViewModelTest.kt @@ -20,11 +20,11 @@ package com.instructure.student.features.assignmentdetails import android.app.Application import android.content.res.Resources import android.util.Log +import android.webkit.MimeTypeMap import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry -import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import com.instructure.canvasapi2.models.Assignment @@ -38,6 +38,7 @@ import com.instructure.canvasapi2.models.Submission import com.instructure.canvasapi2.models.User import com.instructure.canvasapi2.utils.ApiPrefs import com.instructure.canvasapi2.utils.ContextKeeper +import com.instructure.canvasapi2.utils.DataResult import com.instructure.canvasapi2.utils.toApiString import com.instructure.pandautils.R import com.instructure.pandautils.mvvm.ViewState @@ -55,17 +56,21 @@ import com.instructure.student.features.assignments.reminder.AlarmScheduler import com.instructure.student.mobius.common.ui.SubmissionHelper import com.instructure.student.room.StudentDb import com.instructure.student.room.entities.CreateSubmissionEntity +import com.instructure.student.util.getStudioLTITool import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.unmockkAll +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestCoroutineDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.setMain -import org.junit.Assert +import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test @@ -81,7 +86,7 @@ class AssignmentDetailsViewModelTest { private val lifecycleOwner: LifecycleOwner = mockk(relaxed = true) private val lifecycleRegistry = LifecycleRegistry(lifecycleOwner) - private val testDispatcher = TestCoroutineDispatcher() + private val testDispatcher = UnconfinedTestDispatcher() private val savedStateHandle: SavedStateHandle = mockk(relaxed = true) private val assignmentDetailsRepository: AssignmentDetailsRepository = mockk(relaxed = true) @@ -118,6 +123,7 @@ class AssignmentDetailsViewModelTest { } returns MutableLiveData(listOf()) } + @After fun tearDown() { unmockkAll() } @@ -163,8 +169,8 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() - Assert.assertEquals(ViewState.Error(expected), viewModel.state.value) - Assert.assertEquals(expected, (viewModel.state.value as? ViewState.Error)?.errorMessage) + assertEquals(ViewState.Error(expected), viewModel.state.value) + assertEquals(expected, (viewModel.state.value as? ViewState.Error)?.errorMessage) } @Test @@ -179,8 +185,8 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() - Assert.assertEquals(ViewState.Error(authError), viewModel.state.value) - Assert.assertEquals(authError, (viewModel.state.value as? ViewState.Error)?.errorMessage) + assertEquals(ViewState.Error(authError), viewModel.state.value) + assertEquals(authError, (viewModel.state.value as? ViewState.Error)?.errorMessage) } @Test @@ -193,8 +199,8 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() - Assert.assertEquals(ViewState.Success, viewModel.state.value) - Assert.assertEquals(true, viewModel.data.value?.fullLocked) + assertEquals(ViewState.Success, viewModel.state.value) + assertEquals(true, viewModel.data.value?.fullLocked) } @Test @@ -214,9 +220,9 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() - Assert.assertEquals(ViewState.Success, viewModel.state.value) - Assert.assertEquals(false, viewModel.data.value?.fullLocked) - Assert.assertEquals(lockedExplanation, viewModel.data.value?.lockedMessage) + assertEquals(ViewState.Success, viewModel.state.value) + assertEquals(false, viewModel.data.value?.fullLocked) + assertEquals(lockedExplanation, viewModel.data.value?.lockedMessage) } @Test @@ -231,8 +237,8 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() - Assert.assertEquals(ViewState.Success, viewModel.state.value) - Assert.assertEquals(expected, viewModel.data.value?.assignmentName) + assertEquals(ViewState.Success, viewModel.state.value) + assertEquals(expected, viewModel.data.value?.assignmentName) } @Test @@ -247,8 +253,8 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() - Assert.assertEquals(ViewState.Success, viewModel.state.value) - Assert.assertEquals(expected, viewModel.data.value?.assignmentName) + assertEquals(ViewState.Success, viewModel.state.value) + assertEquals(expected, viewModel.data.value?.assignmentName) } @Test @@ -264,8 +270,8 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() - Assert.assertEquals(ViewState.Success, viewModel.state.value) - Assert.assertEquals(true, viewModel.data.value?.hasDraft) + assertEquals(ViewState.Success, viewModel.state.value) + assertEquals(true, viewModel.data.value?.hasDraft) } @Test @@ -286,7 +292,7 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() - Assert.assertEquals(3, viewModel.data.value?.attempts?.size) + assertEquals(3, viewModel.data.value?.attempts?.size) } @Test @@ -307,8 +313,8 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() - Assert.assertEquals(expectedGradeCell, viewModel.data.value?.selectedGradeCellViewData) - Assert.assertEquals(GradeCellViewData.State.EMPTY, viewModel.data.value?.selectedGradeCellViewData?.state) + assertEquals(expectedGradeCell, viewModel.data.value?.selectedGradeCellViewData) + assertEquals(GradeCellViewData.State.EMPTY, viewModel.data.value?.selectedGradeCellViewData?.state) } @Test @@ -330,10 +336,10 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() - Assert.assertEquals(expectedLabelText, viewModel.data.value?.submissionStatusText) - Assert.assertEquals(expectedTint, viewModel.data.value?.submissionStatusTint) - Assert.assertEquals(expectedIcon, viewModel.data.value?.submissionStatusIcon) - Assert.assertEquals(true, viewModel.data.value?.submissionStatusVisible) + assertEquals(expectedLabelText, viewModel.data.value?.submissionStatusText) + assertEquals(expectedTint, viewModel.data.value?.submissionStatusTint) + assertEquals(expectedIcon, viewModel.data.value?.submissionStatusIcon) + assertEquals(true, viewModel.data.value?.submissionStatusVisible) } @Test @@ -352,10 +358,10 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() - Assert.assertEquals(expectedLabelText, viewModel.data.value?.submissionStatusText) - Assert.assertEquals(expectedTint, viewModel.data.value?.submissionStatusTint) - Assert.assertEquals(expectedIcon, viewModel.data.value?.submissionStatusIcon) - Assert.assertEquals(true, viewModel.data.value?.submissionStatusVisible) + assertEquals(expectedLabelText, viewModel.data.value?.submissionStatusText) + assertEquals(expectedTint, viewModel.data.value?.submissionStatusTint) + assertEquals(expectedIcon, viewModel.data.value?.submissionStatusIcon) + assertEquals(true, viewModel.data.value?.submissionStatusVisible) } @Test @@ -380,10 +386,10 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() - Assert.assertEquals(expectedLabelText, viewModel.data.value?.submissionStatusText) - Assert.assertEquals(expectedTint, viewModel.data.value?.submissionStatusTint) - Assert.assertEquals(expectedIcon, viewModel.data.value?.submissionStatusIcon) - Assert.assertEquals(true, viewModel.data.value?.submissionStatusVisible) + assertEquals(expectedLabelText, viewModel.data.value?.submissionStatusText) + assertEquals(expectedTint, viewModel.data.value?.submissionStatusTint) + assertEquals(expectedIcon, viewModel.data.value?.submissionStatusIcon) + assertEquals(true, viewModel.data.value?.submissionStatusVisible) } @Test @@ -405,10 +411,10 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() - Assert.assertEquals(expectedLabelText, viewModel.data.value?.submissionStatusText) - Assert.assertEquals(expectedTint, viewModel.data.value?.submissionStatusTint) - Assert.assertEquals(expectedIcon, viewModel.data.value?.submissionStatusIcon) - Assert.assertEquals(true, viewModel.data.value?.submissionStatusVisible) + assertEquals(expectedLabelText, viewModel.data.value?.submissionStatusText) + assertEquals(expectedTint, viewModel.data.value?.submissionStatusTint) + assertEquals(expectedIcon, viewModel.data.value?.submissionStatusIcon) + assertEquals(true, viewModel.data.value?.submissionStatusVisible) } @Test @@ -441,8 +447,8 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() viewModel.onAttemptSelected(2) - Assert.assertEquals(3, viewModel.data.value?.attempts?.size) - Assert.assertEquals(expectedGradeCellViewData, viewModel.data.value?.selectedGradeCellViewData) + assertEquals(3, viewModel.data.value?.attempts?.size) + assertEquals(expectedGradeCellViewData, viewModel.data.value?.selectedGradeCellViewData) } @Test @@ -457,7 +463,7 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() viewModel.onLtiButtonPressed(expected) - Assert.assertEquals(expected, (viewModel.events.value?.peekContent() as AssignmentDetailAction.NavigateToLtiScreen).url) + assertEquals(expected, (viewModel.events.value?.peekContent() as AssignmentDetailAction.NavigateToLtiScreen).url) } @Test @@ -470,7 +476,7 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() viewModel.onGradeCellClicked() - Assert.assertTrue(viewModel.events.value?.peekContent() is AssignmentDetailAction.NavigateToSubmissionScreen) + assertTrue(viewModel.events.value?.peekContent() is AssignmentDetailAction.NavigateToSubmissionScreen) } @Test @@ -487,7 +493,7 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() viewModel.onGradeCellClicked() - Assert.assertTrue(viewModel.events.value?.peekContent() is AssignmentDetailAction.NavigateToTextEntryScreen) + assertTrue(viewModel.events.value?.peekContent() is AssignmentDetailAction.NavigateToTextEntryScreen) } @Test @@ -504,7 +510,7 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() viewModel.onGradeCellClicked() - Assert.assertTrue(viewModel.events.value?.peekContent() is AssignmentDetailAction.NavigateToUploadStatusScreen) + assertTrue(viewModel.events.value?.peekContent() is AssignmentDetailAction.NavigateToUploadStatusScreen) } @Test @@ -521,7 +527,7 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() viewModel.onGradeCellClicked() - Assert.assertTrue(viewModel.events.value?.peekContent() is AssignmentDetailAction.NavigateToUrlSubmissionScreen) + assertTrue(viewModel.events.value?.peekContent() is AssignmentDetailAction.NavigateToUrlSubmissionScreen) } @Test @@ -537,7 +543,7 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() viewModel.onSubmitButtonClicked() - Assert.assertTrue(viewModel.events.value?.peekContent() is AssignmentDetailAction.NavigateToQuizScreen) + assertTrue(viewModel.events.value?.peekContent() is AssignmentDetailAction.NavigateToQuizScreen) } @Test @@ -551,7 +557,7 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() viewModel.onSubmitButtonClicked() - Assert.assertTrue(viewModel.events.value?.peekContent() is AssignmentDetailAction.NavigateToDiscussionScreen) + assertTrue(viewModel.events.value?.peekContent() is AssignmentDetailAction.NavigateToDiscussionScreen) } @Test @@ -571,7 +577,7 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() viewModel.onSubmitButtonClicked() - Assert.assertTrue(viewModel.events.value?.peekContent() is AssignmentDetailAction.ShowSubmitDialog) + assertTrue(viewModel.events.value?.peekContent() is AssignmentDetailAction.ShowSubmitDialog) } @Test @@ -585,7 +591,7 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() viewModel.onSubmitButtonClicked() - Assert.assertTrue(viewModel.events.value?.peekContent() is AssignmentDetailAction.NavigateToTextEntryScreen) + assertTrue(viewModel.events.value?.peekContent() is AssignmentDetailAction.NavigateToTextEntryScreen) } @Test @@ -599,7 +605,7 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() viewModel.onSubmitButtonClicked() - Assert.assertTrue(viewModel.events.value?.peekContent() is AssignmentDetailAction.NavigateToUrlSubmissionScreen) + assertTrue(viewModel.events.value?.peekContent() is AssignmentDetailAction.NavigateToUrlSubmissionScreen) } @Test @@ -613,7 +619,7 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() viewModel.onSubmitButtonClicked() - Assert.assertTrue(viewModel.events.value?.peekContent() is AssignmentDetailAction.NavigateToAnnotationSubmissionScreen) + assertTrue(viewModel.events.value?.peekContent() is AssignmentDetailAction.NavigateToAnnotationSubmissionScreen) } @Test @@ -627,7 +633,7 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() viewModel.onSubmitButtonClicked() - Assert.assertTrue(viewModel.events.value?.peekContent() is AssignmentDetailAction.ShowMediaDialog) + assertTrue(viewModel.events.value?.peekContent() is AssignmentDetailAction.ShowMediaDialog) } @Test @@ -641,7 +647,7 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() viewModel.onSubmitButtonClicked() - Assert.assertTrue(viewModel.events.value?.peekContent() is AssignmentDetailAction.NavigateToLtiLaunchScreen) + assertTrue(viewModel.events.value?.peekContent() is AssignmentDetailAction.NavigateToLtiLaunchScreen) } @Test @@ -661,11 +667,11 @@ class AssignmentDetailsViewModelTest { liveData.postValue(listOf(getDbSubmission())) - Assert.assertTrue(viewModel.data.value?.attempts?.first()?.data?.isUploading!!) + assertTrue(viewModel.data.value?.attempts?.first()?.data?.isUploading!!) liveData.postValue(listOf(getDbSubmission().copy(errorFlag = true))) - Assert.assertTrue(viewModel.data.value?.attempts?.first()?.data?.isFailed!!) + assertTrue(viewModel.data.value?.attempts?.first()?.data?.isFailed!!) } @Test @@ -687,14 +693,14 @@ class AssignmentDetailsViewModelTest { liveData.postValue(listOf(getDbSubmission())) - Assert.assertTrue(viewModel.data.value?.attempts?.first()?.data?.isUploading!!) + assertTrue(viewModel.data.value?.attempts?.first()?.data?.isUploading!!) val assignment = Assignment(submission = Submission(submissionHistory = listOf(expected))) coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment liveData.postValue(emptyList()) - Assert.assertEquals(expected, viewModel.data.value?.attempts?.last()?.data?.submission) + assertEquals(expected, viewModel.data.value?.attempts?.last()?.data?.submission) } @Test @@ -709,7 +715,7 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() - Assert.assertEquals("20 pts", viewModel.data.value?.points) + assertEquals("20 pts", viewModel.data.value?.points) } @Test @@ -725,7 +731,7 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() - Assert.assertEquals("", viewModel.data.value?.points) + assertEquals("", viewModel.data.value?.points) } @Test @@ -737,7 +743,7 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() - Assert.assertFalse(viewModel.showContent(viewModel.state.value)) + assertFalse(viewModel.showContent(viewModel.state.value)) } @Test @@ -749,7 +755,7 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() - Assert.assertTrue(viewModel.showContent(viewModel.state.value)) + assertTrue(viewModel.showContent(viewModel.state.value)) } @Test @@ -762,7 +768,7 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() - Assert.assertFalse(viewModel.data.value?.submitVisible!!) + assertFalse(viewModel.data.value?.submitVisible!!) } @Test @@ -776,7 +782,7 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() - Assert.assertFalse(viewModel.data.value?.submitVisible!!) + assertFalse(viewModel.data.value?.submitVisible!!) } @Test @@ -789,7 +795,7 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() - Assert.assertFalse(viewModel.data.value?.submitVisible!!) + assertFalse(viewModel.data.value?.submitVisible!!) } @Test @@ -802,7 +808,7 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() - Assert.assertFalse(viewModel.data.value?.showReminders!!) + assertFalse(viewModel.data.value?.showReminders!!) } @Test @@ -819,7 +825,7 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() - Assert.assertTrue(viewModel.data.value?.showReminders!!) + assertTrue(viewModel.data.value?.showReminders!!) } @Test @@ -843,7 +849,7 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() - Assert.assertEquals( + assertEquals( reminderEntities.map { ReminderViewData(it.id, "${it.text} Before") }, viewModel.data.value?.reminders?.map { it.data } ) @@ -866,11 +872,11 @@ class AssignmentDetailsViewModelTest { val viewModel = getViewModel() - Assert.assertEquals(0, viewModel.data.value?.reminders?.size) + assertEquals(0, viewModel.data.value?.reminders?.size) remindersLiveData.value = listOf(ReminderEntity(1, 1, 1, "htmlUrl1", "Assignment 1", "1 day", 1000)) - Assert.assertEquals(ReminderViewData(1, "1 day Before"), viewModel.data.value?.reminders?.first()?.data) + assertEquals(ReminderViewData(1, "1 day Before"), viewModel.data.value?.reminders?.first()?.data) } @Test @@ -889,7 +895,7 @@ class AssignmentDetailsViewModelTest { viewModel.onAddReminderClicked() - Assert.assertEquals(AssignmentDetailAction.ShowReminderDialog, viewModel.events.value?.peekContent()) + assertEquals(AssignmentDetailAction.ShowReminderDialog, viewModel.events.value?.peekContent()) } @Test @@ -933,7 +939,7 @@ class AssignmentDetailsViewModelTest { viewModel.onReminderSelected(ReminderChoice.Custom) - Assert.assertEquals(AssignmentDetailAction.ShowCustomReminderDialog, viewModel.events.value?.peekContent()) + assertEquals(AssignmentDetailAction.ShowCustomReminderDialog, viewModel.events.value?.peekContent()) } @Test @@ -955,7 +961,7 @@ class AssignmentDetailsViewModelTest { viewModel.onReminderSelected(ReminderChoice.Day(3)) - Assert.assertEquals(AssignmentDetailAction.ShowToast("Reminder in past"), viewModel.events.value?.peekContent()) + assertEquals(AssignmentDetailAction.ShowToast("Reminder in past"), viewModel.events.value?.peekContent()) } @Test @@ -981,6 +987,89 @@ class AssignmentDetailsViewModelTest { viewModel.onReminderSelected(ReminderChoice.Day(3)) - Assert.assertEquals(AssignmentDetailAction.ShowToast("Reminder in past"), viewModel.events.value?.peekContent()) + assertEquals(AssignmentDetailAction.ShowToast("Reminder in past"), viewModel.events.value?.peekContent()) } + + @Test + fun `studio disabled if not allowed in assignment`() { + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course + + mockkStatic(Long::getStudioLTITool) + coEvery { course.id.getStudioLTITool() } returns DataResult.Fail() + + val assignment = Assignment(name = "Test assignment", submissionTypesRaw = listOf("online_upload")) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment + + val viewModel = getViewModel() + + val result = viewModel.isStudioAccepted() + + assertFalse(result) + } + + @Test + fun `studio disabled if online upload submission type is not allowed`() { + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course + + mockkStatic(Long::getStudioLTITool) + coEvery { course.id.getStudioLTITool() } returns DataResult.Success(mockk()) + + val assignment = Assignment(name = "Test assignment", submissionTypesRaw = listOf("online_text_entry")) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment + + val viewModel = getViewModel() + + val result = viewModel.isStudioAccepted() + + assertFalse(result) + } + + @Test + fun `studio disabled if no audio visual extension is allowed`() { + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course + + mockkStatic(Long::getStudioLTITool) + coEvery { course.id.getStudioLTITool() } returns DataResult.Success(mockk()) + + mockkStatic(MimeTypeMap::getSingleton) + every { MimeTypeMap.getSingleton() } returns mockk() + every { MimeTypeMap.getSingleton().getMimeTypeFromExtension("pdf") } returns "application/pdf" + + val assignment = Assignment(name = "Test assignment", submissionTypesRaw = listOf("online_upload"), allowedExtensions = listOf("pdf")) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment + + val viewModel = getViewModel() + + val result = viewModel.isStudioAccepted() + + assertFalse(result) + } + + @Test + fun `studio enabled if audio visual extension are allowed`() { + val course = Course(enrollments = mutableListOf(Enrollment(type = Enrollment.EnrollmentType.Student))) + coEvery { assignmentDetailsRepository.getCourseWithGrade(any(), any()) } returns course + + mockkStatic(Long::getStudioLTITool) + coEvery { course.id.getStudioLTITool() } returns DataResult.Success(mockk()) + + mockkStatic(MimeTypeMap::getSingleton) + every { MimeTypeMap.getSingleton() } returns mockk() + every { MimeTypeMap.getSingleton().getMimeTypeFromExtension("mp4") } returns "video/mp4" + every { MimeTypeMap.getSingleton().getMimeTypeFromExtension("mp3") } returns "audio/mp3" + every { MimeTypeMap.getSingleton().getMimeTypeFromExtension("pdf") } returns "application/pdf" + + val assignment = Assignment(name = "Test assignment", submissionTypesRaw = listOf("online_upload"), allowedExtensions = listOf("pdf", "mp3", "mp4")) + coEvery { assignmentDetailsRepository.getAssignment(any(), any(), any(), any()) } returns assignment + + val viewModel = getViewModel() + + val result = viewModel.isStudioAccepted() + + assert(result) + } + } diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/FileExtensions.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/FileExtensions.kt index 542d502d8b..857fec6a06 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/FileExtensions.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/FileExtensions.kt @@ -3,6 +3,7 @@ package com.instructure.pandautils.utils import android.content.Context import android.content.Intent import android.net.Uri +import android.webkit.MimeTypeMap import androidx.core.content.FileProvider import com.instructure.pandautils.R import java.io.File @@ -29,4 +30,9 @@ fun Uri.viewExternally(context: Context, contentType: String, onNoApps: () -> Un intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) val appCount = context.packageManager.queryIntentActivities(intent, 0).size if (appCount > 0) context.startActivity(intent) else onNoApps() +} + +fun isAudioVisualExtension(extension: String): Boolean { + val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) + return type?.startsWith("audio/") == true || type?.startsWith("video/") == true } \ No newline at end of file From 0dafc8372ec74b8355866ba5de8e8a0e40e1608b Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Thu, 25 Jul 2024 16:10:43 +0200 Subject: [PATCH 14/50] [MBL-17486][Parent] Alerts list (#2497) Test plan: Compare with the flutter version. refs: MBL-17486 affects: Parent release note: none --- .../ui/compose/alerts/AlertsListItemTest.kt | 250 ++++++ .../ui/compose/alerts/AlertsScreenTest.kt | 224 ++++++ .../ui/interaction/AlertsInteractionTest.kt | 173 +++++ .../parentapp/ui/pages/AlertsPage.kt | 94 +++ .../parentapp/ui/pages/DashboardPage.kt | 5 + .../parentapp/utils/ParentComposeTest.kt | 3 + .../parentapp/utils/ParentTestExtensions.kt | 6 + .../instructure/parentapp/di/AlertsModule.kt | 35 + .../parentapp/di/DashboardModule.kt | 8 + .../features/alerts/list/AlertsFragment.kt | 37 +- .../features/alerts/list/AlertsRepository.kt | 81 ++ .../features/alerts/list/AlertsScreen.kt | 441 +++++++++++ .../features/alerts/list/AlertsUiState.kt | 51 ++ .../features/alerts/list/AlertsViewModel.kt | 204 +++++ .../features/dashboard/AlertCountUpdater.kt | 34 + .../features/dashboard/DashboardFragment.kt | 12 + .../features/dashboard/DashboardViewData.kt | 3 +- .../features/dashboard/DashboardViewModel.kt | 35 +- .../dashboard/SelectedStudentHolder.kt | 12 +- .../alerts/list/AlertsRepositoryTest.kt | 379 +++++++++ .../alerts/list/AlertsViewModelTest.kt | 730 ++++++++++++++++++ .../courses/list/CoursesViewModelTest.kt | 4 +- .../dashboard/DashboardRepositoryTest.kt | 3 +- .../dashboard/DashboardViewModelTest.kt | 37 +- .../dashboard/TestAlertCountUpdater.kt | 26 + .../dashboard/TestSelectStudentHolder.kt | 4 +- .../canvas/espresso/mockCanvas/MockCanvas.kt | 68 ++ .../endpoints/ObserverAlertsEndpoint.kt | 56 ++ .../mockCanvas/endpoints/UserEndpoints.kt | 10 + .../espresso/mockCanvas/utils/PathUtils.kt | 2 + .../espresso/mockCanvas/utils/Randomizer.kt | 2 + .../canvasapi2/apis/ObserverApi.kt | 41 + .../canvasapi2/apis/UnreadCountAPI.kt | 3 + .../instructure/canvasapi2/di/ApiModule.kt | 5 + .../instructure/canvasapi2/models/Alert.kt | 95 +++ .../canvasapi2/models/AlertThreshold.kt | 29 + libs/pandares/src/main/res/values/strings.xml | 16 + .../pandautils/utils/ComposeExtensions.kt | 21 + 38 files changed, 3214 insertions(+), 25 deletions(-) create mode 100644 apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/AlertsListItemTest.kt create mode 100644 apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/AlertsScreenTest.kt create mode 100644 apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AlertsInteractionTest.kt create mode 100644 apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AlertsPage.kt create mode 100644 apps/parent/src/main/java/com/instructure/parentapp/di/AlertsModule.kt create mode 100644 apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsRepository.kt create mode 100644 apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsScreen.kt create mode 100644 apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsUiState.kt create mode 100644 apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsViewModel.kt create mode 100644 apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/AlertCountUpdater.kt create mode 100644 apps/parent/src/test/java/com/instructure/parentapp/features/alerts/list/AlertsRepositoryTest.kt create mode 100644 apps/parent/src/test/java/com/instructure/parentapp/features/alerts/list/AlertsViewModelTest.kt create mode 100644 apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/TestAlertCountUpdater.kt create mode 100644 automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ObserverAlertsEndpoint.kt create mode 100644 libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ObserverApi.kt create mode 100644 libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Alert.kt create mode 100644 libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/AlertThreshold.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/utils/ComposeExtensions.kt diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/AlertsListItemTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/AlertsListItemTest.kt new file mode 100644 index 0000000000..252a86d226 --- /dev/null +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/AlertsListItemTest.kt @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.instructure.parentapp.ui.compose.alerts + +import android.graphics.Color +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.AlertType +import com.instructure.parentapp.features.alerts.list.AlertsItemUiState +import com.instructure.parentapp.features.alerts.list.AlertsListItem +import com.instructure.parentapp.utils.hasDrawable +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.util.Date +import com.instructure.parentapp.R +import java.text.SimpleDateFormat +import java.util.Locale + +@RunWith(AndroidJUnit4::class) +class AlertsListItemTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun assertAssignmentMissing() { + composeTestRule.setContent { + AlertsListItem(alert = AlertsItemUiState( + alertId = 1L, + title = "Assignment Missing title", + alertType = AlertType.ASSIGNMENT_MISSING, + date = Date(), + observerAlertThreshold = null, + lockedForUser = false, + unread = true, + htmlUrl = null + ), userColor = Color.BLUE, actionHandler = {}) + } + + composeTestRule.waitForIdle() + + composeTestRule.onNodeWithText("Assignment missing").assertIsDisplayed() + composeTestRule.onNodeWithText("Assignment Missing title").assertIsDisplayed() + composeTestRule.onNode(hasDrawable(R.drawable.ic_warning)).assertIsDisplayed() + composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed() + composeTestRule.onNodeWithTag("alertItem").assertHasClickAction() + } + + @Test + fun assertAssignmentGradeHigh() { + composeTestRule.setContent { + AlertsListItem(alert = AlertsItemUiState( + alertId = 1L, + title = "Assignment Grade High title", + alertType = AlertType.ASSIGNMENT_GRADE_HIGH, + date = Date(), + observerAlertThreshold = "90%", + lockedForUser = false, + unread = true, + htmlUrl = null + ), userColor = Color.BLUE, actionHandler = {}) + } + + composeTestRule.onNodeWithText("Assignment Grade Above 90%").assertIsDisplayed() + composeTestRule.onNodeWithText("Assignment Grade High title").assertIsDisplayed() + composeTestRule.onNode(hasDrawable(R.drawable.ic_info)).assertIsDisplayed() + composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed() + composeTestRule.onNodeWithTag("alertItem").assertHasClickAction() + } + + @Test + fun assertAssignmentGradeLow() { + composeTestRule.setContent { + AlertsListItem(alert = AlertsItemUiState( + alertId = 1L, + title = "Assignment Grade Low title", + alertType = AlertType.ASSIGNMENT_GRADE_LOW, + date = Date(), + observerAlertThreshold = "60%", + lockedForUser = false, + unread = true, + htmlUrl = null + ), userColor = Color.BLUE, actionHandler = {}) + } + + composeTestRule.onNodeWithText("Assignment Grade Below 60%").assertIsDisplayed() + composeTestRule.onNodeWithText("Assignment Grade Low title").assertIsDisplayed() + composeTestRule.onNode(hasDrawable(R.drawable.ic_warning)).assertIsDisplayed() + composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed() + composeTestRule.onNodeWithTag("alertItem").assertHasClickAction() + } + + @Test + fun assertCourseGradeHigh() { + composeTestRule.setContent { + AlertsListItem(alert = AlertsItemUiState( + alertId = 1L, + title = "Course Grade High title", + alertType = AlertType.COURSE_GRADE_HIGH, + date = Date(), + observerAlertThreshold = "90%", + lockedForUser = false, + unread = true, + htmlUrl = null + ), userColor = Color.BLUE, actionHandler = {}) + } + + composeTestRule.onNodeWithText("Course Grade Above 90%").assertIsDisplayed() + composeTestRule.onNodeWithText("Course Grade High title").assertIsDisplayed() + composeTestRule.onNode(hasDrawable(R.drawable.ic_info)).assertIsDisplayed() + composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed() + composeTestRule.onNodeWithTag("alertItem").assertHasClickAction() + } + + @Test + fun assertCourseGradeLow() { + composeTestRule.setContent { + AlertsListItem(alert = AlertsItemUiState( + alertId = 1L, + title = "Course Grade Low title", + alertType = AlertType.COURSE_GRADE_LOW, + date = Date(), + observerAlertThreshold = "60%", + lockedForUser = false, + unread = true, + htmlUrl = null + ), userColor = Color.BLUE, actionHandler = {}) + } + + composeTestRule.onNodeWithText("Course Grade Below 60%").assertIsDisplayed() + composeTestRule.onNodeWithText("Course Grade Low title").assertIsDisplayed() + composeTestRule.onNode(hasDrawable(R.drawable.ic_warning)).assertIsDisplayed() + composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed() + composeTestRule.onNodeWithTag("alertItem").assertHasClickAction() + } + + @Test + fun assertCourseAnnouncement() { + composeTestRule.setContent { + AlertsListItem(alert = AlertsItemUiState( + alertId = 1L, + title = "Course Announcement title", + alertType = AlertType.COURSE_ANNOUNCEMENT, + date = Date(), + observerAlertThreshold = null, + lockedForUser = false, + unread = true, + htmlUrl = null + ), userColor = Color.BLUE, actionHandler = {}) + } + + composeTestRule.onNodeWithText("Course Announcement").assertIsDisplayed() + composeTestRule.onNodeWithText("Course Announcement title").assertIsDisplayed() + composeTestRule.onNode(hasDrawable(R.drawable.ic_info)).assertIsDisplayed() + composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed() + composeTestRule.onNodeWithTag("alertItem").assertHasClickAction() + } + + @Test + fun assertInstitutionAnnouncement() { + composeTestRule.setContent { + AlertsListItem(alert = AlertsItemUiState( + alertId = 1L, + title = "Institution Announcement title", + alertType = AlertType.INSTITUTION_ANNOUNCEMENT, + date = Date(), + observerAlertThreshold = null, + lockedForUser = false, + unread = true, + htmlUrl = null + ), userColor = Color.BLUE, actionHandler = {}) + } + + composeTestRule.onNodeWithText("Institution Announcement").assertIsDisplayed() + composeTestRule.onNodeWithText("Institution Announcement title").assertIsDisplayed() + composeTestRule.onNode(hasDrawable(R.drawable.ic_info)).assertIsDisplayed() + composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed() + composeTestRule.onNodeWithTag("alertItem").assertHasClickAction() + } + + @Test + fun assertLockedForUser() { + composeTestRule.setContent { + AlertsListItem(alert = AlertsItemUiState( + alertId = 1L, + title = "Locked for User title", + alertType = AlertType.ASSIGNMENT_MISSING, + date = Date(), + observerAlertThreshold = null, + lockedForUser = true, + unread = true, + htmlUrl = null + ), userColor = Color.BLUE, actionHandler = {}) + } + + composeTestRule.onNodeWithText("Assignment missing").assertIsDisplayed() + composeTestRule.onNodeWithText("Locked for User title").assertIsDisplayed() + composeTestRule.onNode(hasDrawable(R.drawable.ic_lock_lined)).assertIsDisplayed() + composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed() + composeTestRule.onNodeWithTag("alertItem").assertHasClickAction() + } + + @Test + fun assertRead() { + composeTestRule.setContent { + AlertsListItem(alert = AlertsItemUiState( + alertId = 1L, + title = "Institution Announcement title", + alertType = AlertType.INSTITUTION_ANNOUNCEMENT, + date = Date(), + observerAlertThreshold = null, + lockedForUser = false, + unread = false, + htmlUrl = null + ), userColor = Color.BLUE, actionHandler = {}) + } + + composeTestRule.onNodeWithText("Institution Announcement").assertIsDisplayed() + composeTestRule.onNodeWithText("Institution Announcement title").assertIsDisplayed() + composeTestRule.onNode(hasDrawable(R.drawable.ic_info)).assertIsDisplayed() + composeTestRule.onNodeWithText(parseDate(Date())).assertIsDisplayed() + composeTestRule.onNodeWithTag("alertItem").assertHasClickAction() + composeTestRule.onNodeWithTag("unreadIndicator").assertIsNotDisplayed() + } + + private fun parseDate(date: Date): String { + val dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.getDefault()) + val timeFormat = SimpleDateFormat("h:mm a", Locale.getDefault()) + return "${dateFormat.format(date)} at ${timeFormat.format(date)}" + } +} \ No newline at end of file diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/AlertsScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/AlertsScreenTest.kt new file mode 100644 index 0000000000..3f2586ed59 --- /dev/null +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/alerts/AlertsScreenTest.kt @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.instructure.parentapp.ui.compose.alerts + +import android.graphics.Color +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasAnyDescendant +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.canvasapi2.models.AlertType +import com.instructure.parentapp.features.alerts.list.AlertsItemUiState +import com.instructure.parentapp.features.alerts.list.AlertsScreen +import com.instructure.parentapp.features.alerts.list.AlertsUiState +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import java.text.SimpleDateFormat +import java.time.Instant +import java.util.Date +import java.util.Locale + +@ExperimentalMaterialApi +@RunWith(AndroidJUnit4::class) +class AlertsScreenTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun assertError() { + composeTestRule.setContent { + AlertsScreen(uiState = AlertsUiState( + alerts = emptyList(), + isLoading = false, + isError = true, + isRefreshing = false, + studentColor = Color.BLUE, + ), actionHandler = {}) + } + + + composeTestRule.onNodeWithText("There was an error loading your student’s alerts.") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Retry").assertHasClickAction().assertIsDisplayed() + } + + @Test + fun assertLoading() { + composeTestRule.setContent { + AlertsScreen(uiState = AlertsUiState( + alerts = emptyList(), + isLoading = true, + isError = false, + isRefreshing = false, + studentColor = Color.BLUE, + ), actionHandler = {}) + } + + composeTestRule.onNodeWithTag("loading").assertIsDisplayed() + } + + @Test + fun assertEmpty() { + composeTestRule.setContent { + AlertsScreen(uiState = AlertsUiState( + alerts = emptyList(), + isLoading = false, + isError = false, + isRefreshing = false, + studentColor = Color.BLUE, + ), actionHandler = {}) + } + + composeTestRule.onNodeWithText("No Alerts").assertIsDisplayed() + composeTestRule.onNodeWithText("There's nothing to be notified of yet.").assertIsDisplayed() + } + + @Test + fun assertRefreshing() { + composeTestRule.setContent { + AlertsScreen(uiState = AlertsUiState( + alerts = emptyList(), + isLoading = false, + isError = false, + isRefreshing = true, + studentColor = Color.BLUE, + ), actionHandler = {}) + } + + composeTestRule.onNodeWithTag("pullRefreshIndicator").assertIsDisplayed() + } + + @Test + fun assertContent() { + val items = listOf( + AlertsItemUiState( + alertId = 1, + title = "Alert 1", + alertType = AlertType.ASSIGNMENT_GRADE_LOW, + date = Date.from(Instant.parse("2023-09-15T09:02:00Z")), + observerAlertThreshold = "20%", + lockedForUser = false, + unread = false, + htmlUrl = null + ), + AlertsItemUiState( + alertId = 2, + title = "Alert 2", + alertType = AlertType.ASSIGNMENT_GRADE_HIGH, + date = Date.from(Instant.parse("2023-09-16T09:02:00Z")), + observerAlertThreshold = "80%", + lockedForUser = false, + unread = true, + htmlUrl = null + ), + AlertsItemUiState( + alertId = 3, + title = "Alert 3", + alertType = AlertType.COURSE_GRADE_LOW, + date = Date.from(Instant.parse("2023-09-17T09:02:00Z")), + observerAlertThreshold = "20%", + lockedForUser = false, + unread = false, + htmlUrl = null + ), + AlertsItemUiState( + alertId = 4, + title = "Alert 4", + alertType = AlertType.COURSE_GRADE_HIGH, + date = Date.from(Instant.parse("2023-09-18T09:02:00Z")), + observerAlertThreshold = "50%", + lockedForUser = false, + unread = true, + htmlUrl = null + ), + AlertsItemUiState( + alertId = 5, + title = "Alert 5", + alertType = AlertType.ASSIGNMENT_MISSING, + date = Date.from(Instant.parse("2023-09-19T09:02:00Z")), + observerAlertThreshold = null, + lockedForUser = false, + unread = false, + htmlUrl = null + ), + AlertsItemUiState( + alertId = 6, + title = "Alert 6", + alertType = AlertType.COURSE_ANNOUNCEMENT, + date = Date.from(Instant.parse("2023-09-20T09:02:00Z")), + observerAlertThreshold = null, + lockedForUser = false, + unread = true, + htmlUrl = null + ), + AlertsItemUiState( + alertId = 7, + title = "Alert 7", + alertType = AlertType.INSTITUTION_ANNOUNCEMENT, + date = Date.from(Instant.parse("2023-09-21T09:02:00Z")), + observerAlertThreshold = null, + lockedForUser = false, + unread = false, + htmlUrl = null + ) + ) + + composeTestRule.setContent { + AlertsScreen(uiState = AlertsUiState( + alerts = items, + isLoading = false, + isError = false, + isRefreshing = false, + studentColor = Color.BLUE, + ), actionHandler = {}) + } + + items.forEach { + composeTestRule.onNodeWithText(it.title).assertIsDisplayed() + composeTestRule.onNodeWithText(parseAlertType(it.alertType, it.observerAlertThreshold)).assertIsDisplayed() + composeTestRule.onNodeWithText(parseDate(it.date!!)).assertIsDisplayed() + composeTestRule.onNode(hasAnyDescendant(hasText(it.title)).and(hasTestTag("alertItem")), useUnmergedTree = true).assertHasClickAction() + } + } + + private fun parseDate(date: Date): String { + val dateFormat = SimpleDateFormat("MMM d", Locale.getDefault()) + val timeFormat = SimpleDateFormat("h:mm a", Locale.getDefault()) + return "${dateFormat.format(date)} at ${timeFormat.format(date)}" + } + + private fun parseAlertType(alertType: AlertType, threshold: String?): String { + return when(alertType) { + AlertType.ASSIGNMENT_GRADE_LOW -> "Assignment Grade Below $threshold" + AlertType.ASSIGNMENT_GRADE_HIGH -> "Assignment Grade Above $threshold" + AlertType.COURSE_GRADE_LOW -> "Course Grade Below $threshold" + AlertType.COURSE_GRADE_HIGH -> "Course Grade Above $threshold" + AlertType.ASSIGNMENT_MISSING -> "Assignment missing" + AlertType.COURSE_ANNOUNCEMENT -> "Course Announcement" + AlertType.INSTITUTION_ANNOUNCEMENT -> "Institution Announcement" + else -> "Unknown" + } + } + +} \ No newline at end of file diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AlertsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AlertsInteractionTest.kt new file mode 100644 index 0000000000..7a2d099067 --- /dev/null +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/AlertsInteractionTest.kt @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.instructure.parentapp.ui.interaction + +import androidx.compose.ui.platform.ComposeView +import androidx.test.espresso.matcher.ViewMatchers +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils +import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck +import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockCanvas.addObserverAlert +import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.canvasapi2.models.AlertType +import com.instructure.canvasapi2.models.AlertWorkflowState +import com.instructure.parentapp.utils.ParentComposeTest +import com.instructure.parentapp.utils.tokenLogin +import dagger.hilt.android.testing.HiltAndroidTest +import org.hamcrest.Matchers +import org.junit.Test +import java.util.Date + +@HiltAndroidTest +class AlertsInteractionTest : ParentComposeTest() { + + @Test + fun dismissAlert() { + val data = initData() + goToAlerts(data) + + val student = data.students.first() + val observer = data.parents.first() + val course = data.courses.values.first() + + val alert = data.addObserverAlert( + observer, + student, + course, + AlertType.ASSIGNMENT_MISSING, + AlertWorkflowState.UNREAD, + Date(), + null, + false + ) + + alertsPage.refresh() + + alertsPage.assertAlertItemDisplayed(alert.title) + + composeTestRule.waitForIdle() + alertsPage.dismissAlert(alert.title) + + composeTestRule.waitForIdle() + alertsPage.assertSnackbar("Alert dismissed") + alertsPage.assertAlertItemNotDisplayed(alert.title) + alertsPage.refresh() + alertsPage.assertAlertItemNotDisplayed(alert.title) + } + + @Test + fun undoDismiss() { + val data = initData() + goToAlerts(data) + + val student = data.students.first() + val observer = data.parents.first() + val course = data.courses.values.first() + + val alert = data.addObserverAlert( + observer, + student, + course, + AlertType.ASSIGNMENT_MISSING, + AlertWorkflowState.UNREAD, + Date(), + null, + false + ) + + alertsPage.refresh() + + alertsPage.assertAlertItemDisplayed(alert.title) + + composeTestRule.waitForIdle() + alertsPage.dismissAlert(alert.title) + + composeTestRule.waitForIdle() + alertsPage.assertSnackbar("Alert dismissed") + alertsPage.clickUndo() + alertsPage.assertAlertItemDisplayed(alert.title) + + alertsPage.refresh() + alertsPage.assertAlertItemDisplayed(alert.title) + } + + @Test + fun emptyAlerts() { + val data = initData() + goToAlerts(data) + + alertsPage.assertEmptyState() + } + + @Test + fun openAlert() { + val data = initData() + goToAlerts(data) + + val student = data.students.first() + val observer = data.parents.first() + val course = data.courses.values.first() + + val alert = data.addObserverAlert( + observer, + student, + course, + AlertType.ASSIGNMENT_MISSING, + AlertWorkflowState.UNREAD, + Date(), + null, + false + ) + + alertsPage.refresh() + + composeTestRule.waitForIdle() + alertsPage.assertAlertItemDisplayed(alert.title) + alertsPage.assertAlertUnread(alert.title) + alertsPage.clickOnAlert(alert.title) + + //TODO check that we route to the correct screen when ready + } + + private fun initData(): MockCanvas { + return MockCanvas.init(studentCount = 1, parentCount = 1, courseCount = 1) + } + + private fun goToAlerts(data: MockCanvas) { + val parent = data.parents.first() + val token = data.tokenFor(parent)!! + tokenLogin(data.domain, token, parent) + + dashboardPage.clickAlerts() + } + + override fun enableAndConfigureAccessibilityChecks() { + extraAccessibilitySupressions = Matchers.allOf( + AccessibilityCheckResultUtils.matchesCheck( + SpeakableTextPresentCheck::class.java + ), + AccessibilityCheckResultUtils.matchesViews( + ViewMatchers.withParent( + ViewMatchers.withClassName( + Matchers.equalTo(ComposeView::class.java.name) + ) + ) + ) + ) + + super.enableAndConfigureAccessibilityChecks() + } + +} \ No newline at end of file diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AlertsPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AlertsPage.kt new file mode 100644 index 0000000000..09f206e617 --- /dev/null +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/AlertsPage.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.instructure.parentapp.ui.pages + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertIsNotDisplayed +import androidx.compose.ui.test.hasAnyAncestor +import androidx.compose.ui.test.hasAnyDescendant +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTouchInput +import androidx.compose.ui.test.swipeDown +import com.instructure.espresso.assertDisplayed +import com.instructure.espresso.click +import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.onViewWithText + +class AlertsPage(private val composeTestRule: ComposeTestRule) : BasePage() { + + fun assertAlertItemDisplayed(title: String) { + composeTestRule.onNodeWithText(title).assertIsDisplayed() + } + + fun assertAlertItemNotDisplayed(title: String) { + composeTestRule.onNodeWithText(title).assertIsNotDisplayed() + } + + fun assertEmptyState() { + composeTestRule.onNodeWithTag("emptyAlerts").assertIsDisplayed() + } + + fun assertAlertRead(title: String) { + composeTestRule.onNode( + hasTestTag("unreadIndicator") + .and(hasAnyAncestor(hasTestTag("alertItem").and(hasAnyDescendant(hasText(title))))), + useUnmergedTree = true + ).assertIsNotDisplayed() + } + + fun assertAlertUnread(title: String) { + composeTestRule.onNode( + hasTestTag("unreadIndicator") + .and(hasAnyAncestor(hasTestTag("alertItem").and(hasAnyDescendant(hasText(title))))), + useUnmergedTree = true + ).assertIsDisplayed() + } + + fun dismissAlert(title: String) { + composeTestRule.onNode( + hasAnyAncestor(hasAnyDescendant(hasText(title)).and(hasTestTag("alertItem"))).and( + hasTestTag( + "dismissButton" + ) + ), + useUnmergedTree = true + ).performClick() + } + + fun clickOnAlert(title: String) { + composeTestRule.onNode( + hasTestTag("alertItem").and(hasAnyDescendant(hasText(title))), + useUnmergedTree = true + ).performClick() + } + + fun refresh() { + composeTestRule.onRoot().performTouchInput { swipeDown() } + } + + fun assertSnackbar(message: String) { + onViewWithText(message).assertDisplayed() + } + + fun clickUndo() { + onViewWithText("UNDO").click() + } +} \ No newline at end of file diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/DashboardPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/DashboardPage.kt index 849b72219c..d627828de2 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/DashboardPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/DashboardPage.kt @@ -36,6 +36,7 @@ class DashboardPage : BasePage(R.id.drawer_layout) { private val toolbar by OnViewWithId(R.id.toolbar) private val bottomNavigationView by OnViewWithId(R.id.bottom_nav) + private val alertsItem by OnViewWithId(R.id.alerts) fun assertObserverData(user: User) { onViewWithText(user.name).assertDisplayed() @@ -79,4 +80,8 @@ class DashboardPage : BasePage(R.id.drawer_layout) { fun clickInbox() { onViewWithText(R.string.inbox).click() } + + fun clickAlerts() { + alertsItem.click() + } } diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentComposeTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentComposeTest.kt index d65bcdc917..85d6f19e0f 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentComposeTest.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentComposeTest.kt @@ -19,6 +19,7 @@ package com.instructure.parentapp.utils import androidx.compose.ui.test.junit4.createAndroidComposeRule import com.instructure.parentapp.features.login.LoginActivity +import com.instructure.parentapp.ui.pages.AlertsPage import org.junit.Rule @@ -27,5 +28,7 @@ abstract class ParentComposeTest : ParentTest() { @get:Rule(order = 1) val composeTestRule = createAndroidComposeRule() + protected val alertsPage = AlertsPage(composeTestRule) + override fun displaysPageObjects() = Unit } diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTestExtensions.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTestExtensions.kt index dd9292fe80..99c8e6583b 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTestExtensions.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/utils/ParentTestExtensions.kt @@ -17,8 +17,11 @@ package com.instructure.parentapp.utils +import androidx.annotation.DrawableRes +import androidx.compose.ui.test.SemanticsMatcher import com.instructure.canvas.espresso.CanvasTest import com.instructure.canvasapi2.models.User +import com.instructure.pandautils.utils.DrawableId import com.instructure.parentapp.features.login.LoginActivity @@ -35,3 +38,6 @@ fun CanvasTest.tokenLogin(domain: String, token: String, user: User, assertDashb dashboardPage.assertPageObjects() } } + +fun hasDrawable(@DrawableRes id: Int): SemanticsMatcher = + SemanticsMatcher.expectValue(DrawableId, id) diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/AlertsModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/AlertsModule.kt new file mode 100644 index 0000000000..23e6953d61 --- /dev/null +++ b/apps/parent/src/main/java/com/instructure/parentapp/di/AlertsModule.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.parentapp.di + +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.ObserverApi +import com.instructure.parentapp.features.alerts.list.AlertsRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent + +@Module +@InstallIn(ViewModelComponent::class) +class AlertsModule { + + @Provides + fun provideAlertsRepository(observerApi: ObserverApi, courseApi: CourseAPI.CoursesInterface): AlertsRepository { + return AlertsRepository(observerApi, courseApi) + } +} \ No newline at end of file diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/DashboardModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/DashboardModule.kt index 4a84ae2ebb..3fde201287 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/di/DashboardModule.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/di/DashboardModule.kt @@ -19,6 +19,8 @@ package com.instructure.parentapp.di import com.instructure.canvasapi2.apis.EnrollmentAPI import com.instructure.canvasapi2.apis.UnreadCountAPI +import com.instructure.parentapp.features.dashboard.AlertCountUpdater +import com.instructure.parentapp.features.dashboard.AlertCountUpdaterImpl import com.instructure.parentapp.features.dashboard.DashboardRepository import com.instructure.parentapp.features.dashboard.InboxCountUpdater import com.instructure.parentapp.features.dashboard.InboxCountUpdaterImpl @@ -59,4 +61,10 @@ class SelectedStudentHolderModule { fun provideInboxCountUpdater(): InboxCountUpdater { return InboxCountUpdaterImpl() } + + @Provides + @Singleton + fun provideAlertCountUpdater(): AlertCountUpdater { + return AlertCountUpdaterImpl() + } } diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsFragment.kt index e75f2afa7c..74a2c9bdf0 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsFragment.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsFragment.kt @@ -21,21 +21,52 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.compose.material.Text +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import com.google.android.material.snackbar.Snackbar +import com.instructure.pandautils.utils.collectOneOffEvents +import com.instructure.parentapp.R +import com.instructure.parentapp.util.navigation.Navigation +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject - +@AndroidEntryPoint class AlertsFragment : Fragment() { + @Inject + lateinit var navigation: Navigation + + private val viewModel: AlertsViewModel by viewModels() + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { + lifecycleScope.collectOneOffEvents(viewModel.events, ::handleAction) return ComposeView(requireActivity()).apply { setContent { - Text(text = "Alerts") + val uiState by viewModel.uiState.collectAsState() + AlertsScreen(uiState = uiState, actionHandler = viewModel::handleAction) + } + } + } + + private fun handleAction(action: AlertsViewModelAction) { + when (action) { + is AlertsViewModelAction.Navigate -> { + navigation.navigate(activity, action.route) + } + + is AlertsViewModelAction.ShowSnackbar -> { + Snackbar.make(requireView(), action.message, Snackbar.LENGTH_SHORT).apply { + action.action?.let { setAction(it) { action.actionCallback?.invoke() } } + setActionTextColor(resources.getColor(R.color.white, resources.newTheme())) + }.show() } } } diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsRepository.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsRepository.kt new file mode 100644 index 0000000000..70b932d0cf --- /dev/null +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsRepository.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.parentapp.features.alerts.list + +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.ObserverApi +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.Alert +import com.instructure.canvasapi2.models.AlertThreshold +import com.instructure.canvasapi2.models.AlertWorkflowState +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.utils.depaginate + +class AlertsRepository( + private val observerApi: ObserverApi, + private val courseApi: CourseAPI.CoursesInterface +) { + + suspend fun getAlertsForStudent(studentId: Long, forceNetwork: Boolean): List { + val restParams = RestParams(isForceReadFromNetwork = forceNetwork) + val allAlerts = observerApi.getObserverAlerts(studentId, restParams).depaginate { + observerApi.getNextPageObserverAlerts(it, restParams) + }.dataOrThrow.sortedByDescending { it.actionDate } + + val coursesMap = mutableMapOf() + val filteredAlerts = allAlerts.filter { alert -> + if (!alert.isQuantitativeRestrictionApplies()) return@filter true + + alert.getCourseId()?.let { courseId -> + val settings = coursesMap.getOrPut(courseId) { + courseApi.getCourseSettings(courseId, restParams).dataOrNull + } + settings?.restrictQuantitativeData?.not() ?: true + } ?: true + } + + return filteredAlerts + } + + suspend fun getAlertThresholdForStudent( + studentId: Long, + forceNetwork: Boolean + ): List { + val restParams = RestParams(isForceReadFromNetwork = forceNetwork) + return observerApi.getObserverAlertThresholds(studentId, restParams).dataOrNull + ?: emptyList() + } + + suspend fun updateAlertWorkflow(alertId: Long, workflowState: AlertWorkflowState): Alert { + val restParams = RestParams(isForceReadFromNetwork = true) + return observerApi.updateAlertWorkflow( + alertId, + workflowState.name.lowercase(), + restParams + ).dataOrThrow + } + + suspend fun getUnreadAlertCount(studentId: Long): Int { + val alerts = try { + getAlertsForStudent(studentId, true) + } catch (e: Exception) { + emptyList() + } + return alerts.count { it.workflowState == AlertWorkflowState.UNREAD } + } + +} \ No newline at end of file diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsScreen.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsScreen.kt new file mode 100644 index 0000000000..01739d777d --- /dev/null +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsScreen.kt @@ -0,0 +1,441 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +@file:OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class) + +package com.instructure.parentapp.features.alerts.list + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +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.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.instructure.canvasapi2.models.AlertType +import com.instructure.canvasapi2.utils.ContextKeeper +import com.instructure.canvasapi2.utils.DateHelper +import com.instructure.pandautils.R +import com.instructure.pandautils.compose.CanvasTheme +import com.instructure.pandautils.compose.composables.EmptyContent +import com.instructure.pandautils.compose.composables.ErrorContent +import com.instructure.pandautils.compose.composables.Loading +import com.instructure.pandautils.utils.drawableId +import java.util.Date + + +@Composable +fun AlertsScreen( + uiState: AlertsUiState, + actionHandler: (AlertsAction) -> Unit, + modifier: Modifier = Modifier +) { + CanvasTheme { + Scaffold( + backgroundColor = colorResource(id = R.color.backgroundLightest), + content = { padding -> + val pullRefreshState = rememberPullRefreshState( + refreshing = uiState.isRefreshing, + onRefresh = { + actionHandler(AlertsAction.Refresh) + } + ) + Box(modifier = modifier.pullRefresh(pullRefreshState)) { + when { + uiState.isError -> { + ErrorContent( + errorMessage = stringResource(id = R.string.errorLoadingAlerts), + retryClick = { + actionHandler(AlertsAction.Refresh) + }, modifier = Modifier.fillMaxSize() + ) + } + + uiState.isLoading -> { + Loading( + modifier = Modifier + .fillMaxSize() + .testTag("loading"), + color = Color(uiState.studentColor) + ) + } + + uiState.alerts.isEmpty() -> { + EmptyContent( + emptyTitle = stringResource(id = R.string.parentNoAlerts), + emptyMessage = stringResource(id = R.string.parentNoAlersMessage), + imageRes = R.drawable.ic_panda_noalerts, + modifier = Modifier + .fillMaxSize() + .testTag("emptyAlerts") + .verticalScroll(rememberScrollState()) + ) + } + + else -> { + AlertsListContent( + uiState = uiState, + actionHandler = actionHandler, + modifier = Modifier + .padding(padding) + .fillMaxSize() + ) + } + } + PullRefreshIndicator( + refreshing = uiState.isRefreshing, + state = pullRefreshState, + modifier = Modifier + .align(Alignment.TopCenter) + .testTag("pullRefreshIndicator"), + contentColor = Color(uiState.studentColor) + ) + } + + }, + modifier = modifier + ) + } +} + +@Composable +fun AlertsListContent( + uiState: AlertsUiState, + actionHandler: (AlertsAction) -> Unit, + modifier: Modifier = Modifier +) { + LazyColumn( + modifier = modifier + ) { + items(uiState.alerts, key = { it.alertId }) { alert -> + AlertsListItem( + alert = alert, + userColor = uiState.studentColor, + actionHandler = actionHandler, + modifier = Modifier.animateItemPlacement() + ) + Spacer(modifier = Modifier.size(8.dp)) + } + } +} + +@Composable +fun AlertsListItem( + alert: AlertsItemUiState, + userColor: Int, + actionHandler: (AlertsAction) -> Unit, + modifier: Modifier = Modifier +) { + val context = LocalContext.current + + fun alertTitle(alertType: AlertType, alertThreshold: String?): String { + val threshold = alertThreshold.orEmpty() + return when (alertType) { + AlertType.ASSIGNMENT_MISSING -> context.getString(R.string.assignmentMissingAlertTitle) + AlertType.ASSIGNMENT_GRADE_HIGH -> context.getString( + R.string.assignmentGradeHighAlertTitle, + threshold + ) + AlertType.ASSIGNMENT_GRADE_LOW -> context.getString( + R.string.assignmentGradeLowAlertTitle, + threshold + ) + AlertType.COURSE_GRADE_HIGH -> context.getString( + R.string.courseGradeHighAlertTitle, + threshold + ) + AlertType.COURSE_GRADE_LOW -> context.getString( + R.string.courseGradeLowAlertTitle, + threshold + ) + AlertType.COURSE_ANNOUNCEMENT -> context.getString(R.string.courseAnnouncementAlertTitle) + AlertType.INSTITUTION_ANNOUNCEMENT -> context.getString(R.string.institutionAnnouncementAlertTitle) + } + } + + fun alertIcon(alertType: AlertType, lockedForUser: Boolean): Int { + return when { + lockedForUser -> R.drawable.ic_lock_lined + alertType.isAlertInfo() || alertType.isAlertPositive() -> R.drawable.ic_info + alertType.isAlertNegative() -> R.drawable.ic_warning + else -> R.drawable.ic_info + } + } + + fun alertColor(alertType: AlertType): Int { + return when { + alertType.isAlertInfo() -> context.getColor(R.color.textDark) + alertType.isAlertNegative() -> context.getColor(R.color.textDanger) + alertType.isAlertPositive() -> userColor + else -> context.getColor(R.color.textDark) + } + } + + fun dateTime(dateTime: Date): String { + val date = DateHelper.getDayMonthDateString(context, dateTime) + val time = DateHelper.getFormattedTime(context, dateTime) + + return context.getString(R.string.alertDateTime, date, time) + } + + Row(modifier = modifier + .fillMaxWidth() + .clickable(enabled = alert.htmlUrl != null) { + alert.htmlUrl?.let { + actionHandler(AlertsAction.Navigate(alert.alertId, it)) + } + } + .padding(8.dp) + .testTag("alertItem"), + verticalAlignment = Alignment.CenterVertically) { + Row(modifier = Modifier.align(Alignment.Top)) { + if (alert.unread) { + Box( + modifier = Modifier + .size(8.dp) + .clip(CircleShape) + .background(Color(userColor)) + .testTag("unreadIndicator") + ) + } + + val iconId = alertIcon(alert.alertType, alert.lockedForUser) + Icon( + modifier = Modifier + .padding(start = if (alert.unread) 0.dp else 8.dp, end = 32.dp) + .semantics { + drawableId = iconId + }, + painter = painterResource(id = iconId), + contentDescription = null, + tint = Color(alertColor(alert.alertType)) + ) + } + Column(modifier = Modifier.weight(1f)) { + Text( + text = alertTitle(alert.alertType, alert.observerAlertThreshold), + style = TextStyle(color = Color(alertColor(alert.alertType)), fontSize = 12.sp) + ) + Text( + modifier = Modifier.padding(vertical = 4.dp), + text = alert.title, + style = TextStyle(color = colorResource(id = R.color.textDarkest), fontSize = 16.sp) + ) + alert.date?.let { + Text( + text = dateTime(alert.date), + style = TextStyle( + color = colorResource(id = R.color.textDark), + fontSize = 12.sp + ) + ) + } + } + IconButton( + modifier = Modifier + .testTag("dismissButton"), + onClick = { actionHandler(AlertsAction.DismissAlert(alert.alertId)) }) { + Icon( + painter = painterResource(id = R.drawable.ic_close), + tint = colorResource(id = R.color.textDark), + contentDescription = stringResource( + id = R.string.a11y_contentDescription_observerAlertDelete + ) + ) + } + } +} + +@Preview +@Composable +fun AlertsScreenPreview() { + AlertsScreen( + uiState = AlertsUiState( + alerts = listOf( + AlertsItemUiState( + alertId = 1L, + title = "Alert title", + alertType = AlertType.COURSE_ANNOUNCEMENT, + date = Date(), + observerAlertThreshold = null, + lockedForUser = false, + unread = true, + htmlUrl = "" + ), + AlertsItemUiState( + alertId = 2L, + title = "Assignment missing", + alertType = AlertType.ASSIGNMENT_MISSING, + date = Date(), + observerAlertThreshold = null, + lockedForUser = false, + unread = false, + htmlUrl = "" + ), + AlertsItemUiState( + alertId = 3L, + title = "Course grade low", + alertType = AlertType.COURSE_GRADE_LOW, + date = Date(), + observerAlertThreshold = "8", + lockedForUser = false, + unread = false, + htmlUrl = "" + ), + AlertsItemUiState( + alertId = 4L, + title = "Course grade high", + alertType = AlertType.COURSE_GRADE_HIGH, + date = Date(), + observerAlertThreshold = "80%", + lockedForUser = false, + unread = false, + htmlUrl = "" + ), + AlertsItemUiState( + alertId = 5L, + title = "Institution announcement", + alertType = AlertType.INSTITUTION_ANNOUNCEMENT, + date = Date(), + observerAlertThreshold = null, + lockedForUser = false, + unread = false, + htmlUrl = "" + ), + AlertsItemUiState( + alertId = 6L, + title = "Assignment grade low", + alertType = AlertType.ASSIGNMENT_GRADE_LOW, + date = Date(), + observerAlertThreshold = "8", + lockedForUser = false, + unread = false, + htmlUrl = "" + ), + AlertsItemUiState( + alertId = 7L, + title = "Assignment grade high", + alertType = AlertType.ASSIGNMENT_GRADE_HIGH, + date = Date(), + observerAlertThreshold = "80%", + lockedForUser = false, + unread = false, + htmlUrl = "" + ), + AlertsItemUiState( + alertId = 8L, + title = "Locked alert", + alertType = AlertType.COURSE_ANNOUNCEMENT, + date = Date(), + observerAlertThreshold = null, + lockedForUser = true, + unread = false, + htmlUrl = "" + ) + ) + ), + actionHandler = {} + ) +} + +@Preview +@Composable +fun AlertsScreenErrorPreview() { + AlertsScreen( + uiState = AlertsUiState(isError = true), + actionHandler = {} + ) +} + +@Preview +@Composable +fun AlertsScreenEmptyPreview() { + AlertsScreen( + uiState = AlertsUiState(), + actionHandler = {} + ) +} + +@Preview +@Composable +fun AlertsScreenLoadingPreview() { + ContextKeeper.appContext = LocalContext.current + AlertsScreen( + uiState = AlertsUiState(isLoading = true), + actionHandler = {} + ) +} + +@Preview +@Composable +fun AlertsScreenRefreshingPreview() { + AlertsScreen( + uiState = AlertsUiState(isRefreshing = true), + actionHandler = {} + ) +} + +@Preview +@Composable +fun AlertsListItemPreview() { + AlertsListItem( + alert = AlertsItemUiState( + alertId = 1L, + title = "Alert title", + alertType = AlertType.COURSE_ANNOUNCEMENT, + date = Date(), + observerAlertThreshold = null, + lockedForUser = false, + unread = true, + htmlUrl = "" + ), + userColor = Color.Blue.toArgb(), + actionHandler = {} + ) +} \ No newline at end of file diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsUiState.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsUiState.kt new file mode 100644 index 0000000000..f9199fcd11 --- /dev/null +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsUiState.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ package com.instructure.parentapp.features.alerts.list + +import android.graphics.Color +import androidx.annotation.ColorInt +import com.instructure.canvasapi2.models.AlertType +import java.util.Date + +data class AlertsUiState( + val alerts: List = emptyList(), + @ColorInt val studentColor: Int = Color.BLACK, + val isLoading: Boolean = false, + val isError: Boolean = false, + val isRefreshing: Boolean = false +) + +data class AlertsItemUiState( + val alertId: Long, + val title: String, + val alertType: AlertType, + val date: Date?, + val observerAlertThreshold: String?, + val lockedForUser: Boolean, + val unread: Boolean, + val htmlUrl: String? +) + +sealed class AlertsViewModelAction { + data class Navigate(val route: String): AlertsViewModelAction() + data class ShowSnackbar(val message: Int, val action: Int?, val actionCallback: (() -> Unit)?): AlertsViewModelAction() +} + +sealed class AlertsAction { + data object Refresh : AlertsAction() + data class Navigate(val alertId: Long, val route: String) : AlertsAction() + data class DismissAlert(val alertId: Long) : AlertsAction() +} \ No newline at end of file diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsViewModel.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsViewModel.kt new file mode 100644 index 0000000000..6fc4d8beb3 --- /dev/null +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/alerts/list/AlertsViewModel.kt @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.parentapp.features.alerts.list + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.instructure.canvasapi2.models.Alert +import com.instructure.canvasapi2.models.AlertThreshold +import com.instructure.canvasapi2.models.AlertWorkflowState +import com.instructure.canvasapi2.models.User +import com.instructure.pandautils.utils.ColorKeeper +import com.instructure.parentapp.R +import com.instructure.parentapp.features.dashboard.AlertCountUpdater +import com.instructure.parentapp.features.dashboard.SelectedStudentHolder +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class AlertsViewModel @Inject constructor( + private val repository: AlertsRepository, + private val colorKeeper: ColorKeeper, + private val selectedStudentHolder: SelectedStudentHolder, + private val alertCountUpdater: AlertCountUpdater +) : ViewModel() { + + private val _uiState = MutableStateFlow(AlertsUiState()) + val uiState = _uiState.asStateFlow() + + private val _events = Channel() + val events = _events.receiveAsFlow() + + private var selectedStudent: User? = null + private var thresholds: Map = emptyMap() + + init { + viewModelScope.launch { + selectedStudentHolder.selectedStudentFlow.collectLatest { + studentChanged(it) + } + } + } + + private suspend fun studentChanged(student: User?) { + if (selectedStudent != student) { + selectedStudent = student + _uiState.update { + it.copy( + studentColor = colorKeeper.getOrGenerateUserColor(student).textAndIconColor(), + isLoading = true + ) + } + loadThresholds() + loadAlerts() + } + } + + private suspend fun loadThresholds(forceNetwork: Boolean = false) { + selectedStudent?.let { student -> + val thresholds = repository.getAlertThresholdForStudent(student.id, forceNetwork) + this.thresholds = thresholds.associateBy { it.id } + } + } + + private suspend fun loadAlerts(forceNetwork: Boolean = false) { + selectedStudent?.let { student -> + try { + val alerts = repository.getAlertsForStudent(student.id, forceNetwork) + val alertItems = alerts.map { createAlertItem(it) } + _uiState.update { + it.copy( + alerts = alertItems, + isLoading = false, + isError = false, + isRefreshing = false, + ) + } + } catch (e: Exception) { + setError() + } + } ?: setError() + + alertCountUpdater.updateShouldRefreshAlertCount(true) + } + + private fun setError() { + _uiState.update { + it.copy(isLoading = false, isError = true, isRefreshing = false, alerts = emptyList()) + } + } + + fun handleAction(action: AlertsAction) { + when (action) { + is AlertsAction.Navigate -> { + viewModelScope.launch { + _events.send(AlertsViewModelAction.Navigate(action.route)) + markAlertRead(action.alertId) + alertCountUpdater.updateShouldRefreshAlertCount(true) + } + } + + is AlertsAction.Refresh -> { + viewModelScope.launch { + _uiState.update { it.copy(isRefreshing = true) } + loadThresholds(true) + loadAlerts(true) + } + } + + is AlertsAction.DismissAlert -> { + viewModelScope.launch { + dismissAlert(action.alertId) + } + } + } + } + + private suspend fun markAlertRead(alertId: Long) { + try { + _uiState.update { uiState -> + uiState.copy( + alerts = uiState.alerts.map { alertItem -> + if (alertItem.alertId == alertId) alertItem.copy(unread = false) else alertItem + } + ) + } + repository.updateAlertWorkflow(alertId, AlertWorkflowState.READ) + alertCountUpdater.updateShouldRefreshAlertCount(true) + } catch (e: Exception) { + //No need to do anything. The alert will stay read. + } + } + + private suspend fun dismissAlert(alertId: Long) { + fun resetAlert(alert: AlertsItemUiState) { + val alerts = _uiState.value.alerts.toMutableList() + alerts.add(alert) + alerts.sortByDescending { it.date } + _uiState.update { it.copy(alerts = alerts) } + viewModelScope.launch { + alertCountUpdater.updateShouldRefreshAlertCount(true) + } + } + + val alerts = _uiState.value.alerts.toMutableList() + val alert = alerts.find { it.alertId == alertId } ?: return + alerts.removeIf { it.alertId == alertId } + _uiState.update { it.copy(alerts = alerts) } + + try { + repository.updateAlertWorkflow(alertId, AlertWorkflowState.DISMISSED) + alertCountUpdater.updateShouldRefreshAlertCount(true) + _events.send(AlertsViewModelAction.ShowSnackbar(R.string.alertDismissMessage, R.string.alertDismissAction) { + viewModelScope.launch { + try { + repository.updateAlertWorkflow( + alert.alertId, + if (alert.unread) AlertWorkflowState.UNREAD else AlertWorkflowState.READ + ) + resetAlert(alert) + } catch (e: Exception) { + _events.send(AlertsViewModelAction.ShowSnackbar(R.string.alertDismissActionErrorMessage, null, null)) + } + } + }) + } catch (e: Exception) { + _events.send(AlertsViewModelAction.ShowSnackbar(R.string.alertDismissErrorMessage, null, null)) + resetAlert(alert) + } + } + + private fun createAlertItem(alert: Alert): AlertsItemUiState { + return AlertsItemUiState( + alertId = alert.id, + title = alert.title, + alertType = alert.alertType, + date = alert.actionDate, + observerAlertThreshold = thresholds[alert.observerAlertThresholdId]?.threshold, + lockedForUser = alert.lockedForUser, + unread = alert.workflowState == AlertWorkflowState.UNREAD, + htmlUrl = alert.htmlUrl + ) + } +} \ No newline at end of file diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/AlertCountUpdater.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/AlertCountUpdater.kt new file mode 100644 index 0000000000..7873738602 --- /dev/null +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/AlertCountUpdater.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ package com.instructure.parentapp.features.dashboard + +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow + +interface AlertCountUpdater { + val shouldRefreshAlertCountFlow: SharedFlow + suspend fun updateShouldRefreshAlertCount(shouldRefresh: Boolean) +} + +class AlertCountUpdaterImpl : AlertCountUpdater { + private val _shouldRefreshAlertCountFlow = MutableSharedFlow(replay = 1) + override val shouldRefreshAlertCountFlow = _shouldRefreshAlertCountFlow.asSharedFlow() + + override suspend fun updateShouldRefreshAlertCount(shouldRefresh: Boolean) { + _shouldRefreshAlertCountFlow.emit(shouldRefresh) + } +} \ No newline at end of file diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardFragment.kt index 95f65ab2c2..19b50dedfd 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardFragment.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardFragment.kt @@ -99,12 +99,22 @@ class DashboardFragment : Fragment(), NavigationCallbacks { setupNavigationDrawerHeader(it.userViewData) setupAppColors(it.selectedStudent) updateUnreadCount(it.unreadCount) + updateAlertCount(it.alertCount) } } handleDeeplink() } + private fun updateAlertCount(alertCount: Int) { + val badge = binding.bottomNav.getOrCreateBadge(R.id.alerts) + badge.verticalOffset = 10 + badge.horizontalOffset = 10 + badge.setVisible(alertCount != 0, true) + badge.maxNumber = 99 + badge.number = alertCount + } + private fun updateUnreadCount(unreadCount: Int) { val unreadCountText = if (unreadCount <= 99) unreadCount.toString() else requireContext().getString(R.string.inboxUnreadCountMoreThan99) inboxBadge?.visibility = if (unreadCount == 0) View.GONE else View.VISIBLE @@ -237,6 +247,8 @@ class DashboardFragment : Fragment(), NavigationCallbacks { gradientDrawable?.setStroke(2.toPx, color) binding.unreadCountBadge.background = gradientDrawable binding.unreadCountBadge.setTextColor(color) + + binding.bottomNav.getOrCreateBadge(R.id.alerts).backgroundColor = color } private fun openNavigationDrawer() { diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewData.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewData.kt index 93313c2573..261d05d49d 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewData.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewData.kt @@ -25,7 +25,8 @@ data class DashboardViewData( val studentSelectorExpanded: Boolean = false, val studentItems: List = emptyList(), val selectedStudent: User? = null, - val unreadCount: Int = 0 + val unreadCount: Int = 0, + val alertCount: Int = 0 ) data class StudentItemViewData( diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewModel.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewModel.kt index 7d8535d17a..43857cb99f 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewModel.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/DashboardViewModel.kt @@ -28,6 +28,7 @@ import com.instructure.loginapi.login.util.PreviousUsersUtils import com.instructure.pandautils.mvvm.ViewState import com.instructure.pandautils.utils.orDefault import com.instructure.parentapp.R +import com.instructure.parentapp.features.alerts.list.AlertsRepository import com.instructure.parentapp.util.ParentPrefs import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext @@ -42,11 +43,13 @@ import javax.inject.Inject class DashboardViewModel @Inject constructor( @ApplicationContext private val context: Context, private val repository: DashboardRepository, + private val alertsRepository: AlertsRepository, private val previousUsersUtils: PreviousUsersUtils, private val apiPrefs: ApiPrefs, private val parentPrefs: ParentPrefs, private val selectedStudentHolder: SelectedStudentHolder, - private val inboxCountUpdater: InboxCountUpdater + private val inboxCountUpdater: InboxCountUpdater, + private val alertCountUpdater: AlertCountUpdater ) : ViewModel() { private val _data = MutableStateFlow(DashboardViewData()) @@ -63,7 +66,7 @@ class DashboardViewModel @Inject constructor( private fun loadData() { viewModelScope.launch { - inboxCountUpdater.shouldRefreshInboxCountFlow.collect {shouldUpdate -> + inboxCountUpdater.shouldRefreshInboxCountFlow.collect { shouldUpdate -> if (shouldUpdate) { updateUnreadCount() inboxCountUpdater.updateShouldRefreshInboxCount(false) @@ -71,12 +74,22 @@ class DashboardViewModel @Inject constructor( } } + viewModelScope.launch { + alertCountUpdater.shouldRefreshAlertCountFlow.collect { shouldUpdate -> + if (shouldUpdate) { + updateAlertCount() + alertCountUpdater.updateShouldRefreshAlertCount(false) + } + } + } + viewModelScope.tryLaunch { _state.value = ViewState.Loading setupUserInfo() loadStudents() updateUnreadCount() + updateAlertCount() if (_data.value.studentItems.isEmpty()) { _state.value = ViewState.Empty( @@ -151,15 +164,16 @@ class DashboardViewModel @Inject constructor( currentUser?.let { previousUsersUtils.add(context, it.copy(selectedStudentId = student.id)) } - viewModelScope.launch { - selectedStudentHolder.updateSelectedStudent(student) - } _data.update { it.copy( studentSelectorExpanded = false, selectedStudent = student ) } + viewModelScope.launch { + selectedStudentHolder.updateSelectedStudent(student) + updateAlertCount() + } } private suspend fun updateUnreadCount() { @@ -171,6 +185,17 @@ class DashboardViewModel @Inject constructor( } } + private suspend fun updateAlertCount() { + _data.value.selectedStudent?.id?.let { + val alertCount = alertsRepository.getUnreadAlertCount(it) + _data.update { + it.copy( + alertCount = alertCount + ) + } + } + } + fun toggleStudentSelector() { _data.update { it.copy(studentSelectorExpanded = !it.studentSelectorExpanded) diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/SelectedStudentHolder.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/SelectedStudentHolder.kt index 15d965979d..d26db3e324 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/SelectedStudentHolder.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/dashboard/SelectedStudentHolder.kt @@ -19,18 +19,18 @@ package com.instructure.parentapp.features.dashboard import com.instructure.canvasapi2.models.User -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow interface SelectedStudentHolder { - val selectedStudentFlow: SharedFlow + val selectedStudentFlow: StateFlow suspend fun updateSelectedStudent(user: User) } class SelectedStudentHolderImpl : SelectedStudentHolder { - private val _selectedStudentFlow = MutableSharedFlow(replay = 1) - override val selectedStudentFlow = _selectedStudentFlow.asSharedFlow() + private val _selectedStudentFlow = MutableStateFlow(null) + override val selectedStudentFlow = _selectedStudentFlow.asStateFlow() override suspend fun updateSelectedStudent(user: User) { _selectedStudentFlow.emit(user) diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/alerts/list/AlertsRepositoryTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/alerts/list/AlertsRepositoryTest.kt new file mode 100644 index 0000000000..0861ad0d2b --- /dev/null +++ b/apps/parent/src/test/java/com/instructure/parentapp/features/alerts/list/AlertsRepositoryTest.kt @@ -0,0 +1,379 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.parentapp.features.alerts.list + +import com.instructure.canvasapi2.apis.CourseAPI +import com.instructure.canvasapi2.apis.ObserverApi +import com.instructure.canvasapi2.models.Alert +import com.instructure.canvasapi2.models.AlertThreshold +import com.instructure.canvasapi2.models.AlertType +import com.instructure.canvasapi2.models.AlertWorkflowState +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.LinkHeaders +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import java.lang.IllegalStateException +import java.time.Instant +import java.util.Date + +class AlertsRepositoryTest { + + private val observerApi: ObserverApi = mockk(relaxed = true) + private val courseApi: CourseAPI.CoursesInterface = mockk(relaxed = true) + + private lateinit var alertsRepository: AlertsRepository + + @Before + fun setup() { + createRepository() + + coEvery { courseApi.getCourse(any(), any()) } returns DataResult.Success(Course(id = 1L)) + } + + @Test + fun `getAlertsForStudent should return an ordered list of alerts`() = runTest { + val alerts = listOf( + Alert( + id = 1, + actionDate = Date.from( + Instant.parse("2024-01-03T00:00:00Z") + ), + title = "Alert 1", + workflowState = AlertWorkflowState.READ, + alertType = AlertType.ASSIGNMENT_MISSING, + htmlUrl = "https://example.com/alert1", + contextId = 1L, + contextType = "Course", + lockedForUser = false, + observerAlertThresholdId = 1L, + observerId = 1L, + userId = 2L + ), + Alert( + id = 1, + actionDate = Date.from( + Instant.parse("2024-01-01T00:00:00Z") + ), + title = "Alert 2", + workflowState = AlertWorkflowState.READ, + alertType = AlertType.ASSIGNMENT_MISSING, + htmlUrl = "https://example.com/alert2", + contextId = 1L, + contextType = "Course", + lockedForUser = false, + observerAlertThresholdId = 1L, + observerId = 1L, + userId = 2L + ), + Alert( + id = 1, + actionDate = Date.from( + Instant.parse("2024-01-02T00:00:00Z") + ), + title = "Alert 3", + workflowState = AlertWorkflowState.READ, + alertType = AlertType.ASSIGNMENT_MISSING, + htmlUrl = "https://example.com/alert3", + contextId = 1L, + contextType = "Course", + lockedForUser = false, + observerAlertThresholdId = 1L, + observerId = 1L, + userId = 2L + ), + ) + val expected = alerts.sortedByDescending { it.actionDate } + + coEvery { observerApi.getObserverAlerts(1L, any()) } returns DataResult.Success(alerts) + + val result = alertsRepository.getAlertsForStudent(1L, false) + + assertEquals(expected, result) + } + + @Test + fun `get alerts depaginates`() = runTest { + val page1 = listOf( + Alert( + id = 1, + actionDate = Date.from( + Instant.parse("2024-01-01T00:00:00Z") + ), + title = "Alert 1", + workflowState = AlertWorkflowState.READ, + alertType = AlertType.ASSIGNMENT_MISSING, + htmlUrl = "https://example.com/alert1", + contextId = 1L, + contextType = "Course", + lockedForUser = false, + observerAlertThresholdId = 1L, + observerId = 1L, + userId = 2L + ) + ) + val page2 = listOf( + Alert( + id = 2, + actionDate = Date.from( + Instant.parse("2024-01-03T00:00:00Z") + ), + title = "Alert 2", + workflowState = AlertWorkflowState.READ, + alertType = AlertType.ASSIGNMENT_MISSING, + htmlUrl = "https://example.com/alert2", + contextId = 1L, + contextType = "Course", + lockedForUser = false, + observerAlertThresholdId = 1L, + observerId = 1L, + userId = 2L + ) + ) + + val expected = (page1 + page2).sortedByDescending { it.actionDate } + + coEvery { observerApi.getObserverAlerts(1L, any()) } returns DataResult.Success(page1, linkHeaders = LinkHeaders(nextUrl = "page_2_url")) + coEvery { observerApi.getNextPageObserverAlerts("page_2_url", any()) } returns DataResult.Success(page2) + + val result = alertsRepository.getAlertsForStudent(1L, false) + + assertEquals(expected, result) + } + + @Test(expected = IllegalStateException::class) + fun `get alerts throw exception if call fails`() = runTest { + coEvery { observerApi.getObserverAlerts(1L, any()) } returns DataResult.Fail() + + alertsRepository.getAlertsForStudent(1L, false) + } + + @Test + fun `get alert thresholds`() = runTest { + val expected = listOf( + AlertThreshold( + id = 1, + observerId = 1, + threshold = "3", + alertType = AlertType.ASSIGNMENT_GRADE_LOW, + userId = 2 + ), + AlertThreshold( + id = 2, + observerId = 1, + threshold = "5", + alertType = AlertType.ASSIGNMENT_GRADE_HIGH, + userId = 2 + ) + ) + + coEvery { observerApi.getObserverAlertThresholds(1L, any()) } returns DataResult.Success(expected) + + val result = alertsRepository.getAlertThresholdForStudent(1L, false) + + assertEquals(expected, result) + } + + @Test + fun `get alert thresholds returns empty list if call fails`() = runTest { + coEvery { observerApi.getObserverAlertThresholds(1L, any()) } returns DataResult.Fail() + + val result = alertsRepository.getAlertThresholdForStudent(1L, false) + + assertEquals(emptyList(), result) + } + + @Test + fun `update alert workflow state`() = runTest { + val alert = Alert( + id = 1, + actionDate = Date.from( + Instant.parse("2024-01-01T00:00:00Z") + ), + title = "Alert 1", + workflowState = AlertWorkflowState.READ, + alertType = AlertType.ASSIGNMENT_MISSING, + htmlUrl = "https://example.com/alert1", + contextId = 1L, + contextType = "Course", + lockedForUser = false, + observerAlertThresholdId = 1L, + observerId = 1L, + userId = 2L + ) + val expected = alert.copy(workflowState = AlertWorkflowState.UNREAD) + + coEvery { observerApi.updateAlertWorkflow(1L, "unread", any()) } returns DataResult.Success(expected) + + val result = alertsRepository.updateAlertWorkflow(1L, AlertWorkflowState.UNREAD) + + coVerify { + observerApi.updateAlertWorkflow(1L, "unread", any()) + } + + assertEquals(expected, result) + } + + @Test(expected = IllegalStateException::class) + fun `update alert workflow state throws exception if call fails`() = runTest { + coEvery { observerApi.updateAlertWorkflow(1L, "unread", any()) } returns DataResult.Fail() + + alertsRepository.updateAlertWorkflow(1L, AlertWorkflowState.UNREAD) + } + + @Test + fun `get alerts count`() = runTest { + val alerts = listOf( + Alert( + id = 1, + actionDate = Date.from( + Instant.parse("2024-01-03T00:00:00Z") + ), + title = "Alert 1", + workflowState = AlertWorkflowState.READ, + alertType = AlertType.ASSIGNMENT_MISSING, + htmlUrl = "https://example.com/alert1", + contextId = 1L, + contextType = "Course", + lockedForUser = false, + observerAlertThresholdId = 1L, + observerId = 1L, + userId = 2L + ), + Alert( + id = 1, + actionDate = Date.from( + Instant.parse("2024-01-01T00:00:00Z") + ), + title = "Alert 2", + workflowState = AlertWorkflowState.UNREAD, + alertType = AlertType.ASSIGNMENT_MISSING, + htmlUrl = "https://example.com/alert2", + contextId = 1L, + contextType = "Course", + lockedForUser = false, + observerAlertThresholdId = 1L, + observerId = 1L, + userId = 2L + ), + Alert( + id = 1, + actionDate = Date.from( + Instant.parse("2024-01-02T00:00:00Z") + ), + title = "Alert 3", + workflowState = AlertWorkflowState.UNREAD, + alertType = AlertType.ASSIGNMENT_MISSING, + htmlUrl = "https://example.com/alert3", + contextId = 1L, + contextType = "Course", + lockedForUser = false, + observerAlertThresholdId = 1L, + observerId = 1L, + userId = 2L + ), + ) + coEvery { observerApi.getObserverAlerts(any(), any()) } returns DataResult.Success(alerts) + + val result = alertsRepository.getUnreadAlertCount(1L) + + assertEquals(2, result) + } + + @Test + fun `alert count set to 0 if call fails`() = runTest { + + coEvery { observerApi.getObserverAlerts(any(), any()) } returns DataResult.Fail() + + val result = alertsRepository.getUnreadAlertCount(1L) + + assertEquals(0, result) + } + + @Test + fun `filter quantitative data if restricted`() = runTest { + val alerts = listOf( + Alert( + id = 1, + actionDate = Date.from( + Instant.parse("2024-01-03T00:00:00Z") + ), + title = "Alert 1", + workflowState = AlertWorkflowState.READ, + alertType = AlertType.COURSE_GRADE_LOW, + htmlUrl = "https://example.com/alert1", + contextId = 1L, + contextType = "Course", + lockedForUser = false, + observerAlertThresholdId = 1L, + observerId = 1L, + userId = 2L + ), + Alert( + id = 2, + actionDate = Date.from( + Instant.parse("2024-01-01T00:00:00Z") + ), + title = "Alert 2", + workflowState = AlertWorkflowState.UNREAD, + alertType = AlertType.ASSIGNMENT_MISSING, + htmlUrl = "https://example.com/alert2", + contextId = 2L, + contextType = "Course", + lockedForUser = false, + observerAlertThresholdId = 2L, + observerId = 1L, + userId = 2L + ), + Alert( + id = 3, + actionDate = Date.from( + Instant.parse("2024-01-02T00:00:00Z") + ), + title = "Alert 3", + workflowState = AlertWorkflowState.UNREAD, + alertType = AlertType.COURSE_GRADE_HIGH, + htmlUrl = "https://example.com/alert3", + contextId = 2L, + contextType = "Course", + lockedForUser = false, + observerAlertThresholdId = 3L, + observerId = 1L, + userId = 2L + ), + ) + + coEvery { observerApi.getObserverAlerts(any(), any()) } returns DataResult.Success(alerts) + coEvery { courseApi.getCourseSettings(1L, any()) } returns DataResult.Success(CourseSettings(restrictQuantitativeData = false)) + coEvery { courseApi.getCourseSettings(2L, any()) } returns DataResult.Success(CourseSettings(restrictQuantitativeData = true)) + + val result = alertsRepository.getAlertsForStudent(1L, false) + + assertEquals(2, result.size) + assertEquals(alerts.subList(0, 2), result) + } + + private fun createRepository() { + alertsRepository = AlertsRepository(observerApi, courseApi) + } +} \ No newline at end of file diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/alerts/list/AlertsViewModelTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/alerts/list/AlertsViewModelTest.kt new file mode 100644 index 0000000000..a09c62ecf6 --- /dev/null +++ b/apps/parent/src/test/java/com/instructure/parentapp/features/alerts/list/AlertsViewModelTest.kt @@ -0,0 +1,730 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.parentapp.features.alerts.list + +import android.graphics.Color +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import com.instructure.canvasapi2.models.Alert +import com.instructure.canvasapi2.models.AlertThreshold +import com.instructure.canvasapi2.models.AlertType +import com.instructure.canvasapi2.models.AlertWorkflowState +import com.instructure.canvasapi2.models.User +import com.instructure.pandautils.utils.ColorKeeper +import com.instructure.pandautils.utils.ThemedColor +import com.instructure.parentapp.R +import com.instructure.parentapp.features.dashboard.AlertCountUpdater +import com.instructure.parentapp.features.dashboard.TestSelectStudentHolder +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import java.time.Instant +import java.util.Date + +@ExperimentalCoroutinesApi +class AlertsViewModelTest { + + @get:Rule + val instantExecutorRule = InstantTaskExecutorRule() + + private val lifecycleOwner: LifecycleOwner = mockk(relaxed = true) + private val lifecycleRegistry = LifecycleRegistry(lifecycleOwner) + private val testDispatcher = UnconfinedTestDispatcher() + + private val repository: AlertsRepository = mockk(relaxed = true) + private val colorKeeper: ColorKeeper = mockk(relaxed = true) + private val alertCountUpdater: AlertCountUpdater = mockk(relaxed = true) + private val selectedStudentFlow = MutableStateFlow(null) + private val selectedStudentHolder = TestSelectStudentHolder(selectedStudentFlow) + + private lateinit var viewModel: AlertsViewModel + + @Before + fun setup() { + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) + Dispatchers.setMain(testDispatcher) + + coEvery { colorKeeper.getOrGenerateUserColor(any()) } returns ThemedColor(1, 1) + coEvery { repository.getAlertThresholdForStudent(any(), any()) } returns emptyList() + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun `Load alerts on student change`() = runTest { + val student = User(1L) + + val alerts = listOf( + Alert( + id = 1, + actionDate = Date.from( + Instant.parse("2024-01-03T00:00:00Z") + ), + title = "Alert 1", + workflowState = AlertWorkflowState.READ, + alertType = AlertType.ASSIGNMENT_MISSING, + htmlUrl = "https://example.com/alert1", + contextId = 1L, + contextType = "Course", + lockedForUser = false, + observerAlertThresholdId = 1L, + observerId = 1L, + userId = 2L + ), + Alert( + id = 1, + actionDate = Date.from( + Instant.parse("2024-01-01T00:00:00Z") + ), + title = "Alert 2", + workflowState = AlertWorkflowState.UNREAD, + alertType = AlertType.ASSIGNMENT_GRADE_LOW, + htmlUrl = "https://example.com/alert2", + contextId = 1L, + contextType = "Course", + lockedForUser = false, + observerAlertThresholdId = 2L, + observerId = 1L, + userId = 2L + ), + ) + + val thresholds = listOf( + AlertThreshold( + id = 1L, + observerId = 1L, + threshold = null, + alertType = AlertType.ASSIGNMENT_MISSING, + userId = 1L + ), + AlertThreshold( + id = 2L, + observerId = 1L, + threshold = "50%", + alertType = AlertType.ASSIGNMENT_GRADE_LOW, + userId = 1L + ) + ) + + coEvery { + repository.getAlertsForStudent(student.id, any()) + } returns alerts + + coEvery { repository.getAlertThresholdForStudent(student.id, any()) } returns thresholds + + + createViewModel() + selectedStudentFlow.emit(student) + + val expected = AlertsUiState( + isLoading = false, + isError = false, + alerts = alerts.map { + AlertsItemUiState( + alertId = it.id, + title = it.title, + alertType = it.alertType, + date = it.actionDate, + observerAlertThreshold = thresholds.find { threshold -> threshold.alertType == it.alertType }?.threshold, + lockedForUser = it.lockedForUser, + unread = it.workflowState == AlertWorkflowState.UNREAD, + htmlUrl = it.htmlUrl + ) + }.sortedByDescending { it.date }, + studentColor = 1 + ) + + assertEquals(expected, viewModel.uiState.value) + } + + @Test + fun `Empty state`() = runTest { + val student = User(1L) + + coEvery { + repository.getAlertsForStudent(student.id, any()) + } returns emptyList() + + createViewModel() + selectedStudentFlow.emit(student) + + val expected = AlertsUiState( + isLoading = false, + isError = false, + alerts = emptyList(), + studentColor = 1 + ) + + assertEquals(expected, viewModel.uiState.value) + } + + @Test + fun `Error state if the student is not set`() = runTest { + createViewModel() + + viewModel.handleAction(AlertsAction.Refresh) + + val expected = AlertsUiState( + isLoading = false, + isError = true, + alerts = emptyList(), + studentColor = Color.BLACK + ) + + assertEquals(expected, viewModel.uiState.value) + } + + @Test + fun `Error state if getting alerts fail`() = runTest { + val student = User(1L) + + coEvery { + repository.getAlertsForStudent(student.id, any()) + } throws Exception() + + createViewModel() + selectedStudentFlow.emit(student) + + val expected = AlertsUiState( + isLoading = false, + isError = true, + alerts = emptyList(), + studentColor = 1 + ) + + assertEquals(expected, viewModel.uiState.value) + } + + @Test + fun `Refresh data`() = runTest { + val student = User(1L) + + val alerts = listOf( + Alert( + id = 1, + actionDate = Date.from( + Instant.parse("2024-01-03T00:00:00Z") + ), + title = "Alert 1", + workflowState = AlertWorkflowState.READ, + alertType = AlertType.ASSIGNMENT_MISSING, + htmlUrl = "https://example.com/alert1", + contextId = 1L, + contextType = "Course", + lockedForUser = false, + observerAlertThresholdId = 1L, + observerId = 1L, + userId = 2L + ) + ) + + coEvery { repository.getAlertsForStudent(student.id, any()) } returns emptyList() + + createViewModel() + selectedStudentFlow.emit(student) + + val expected = AlertsUiState( + isLoading = false, + isError = false, + alerts = emptyList(), + studentColor = 1 + ) + assertEquals(expected, viewModel.uiState.value) + + coEvery { repository.getAlertsForStudent(student.id, any()) } returns alerts + + viewModel.handleAction(AlertsAction.Refresh) + + val expectedRefreshed = AlertsUiState( + isLoading = false, + isError = false, + alerts = alerts.map { + AlertsItemUiState( + alertId = it.id, + title = it.title, + alertType = it.alertType, + date = it.actionDate, + observerAlertThreshold = null, + lockedForUser = it.lockedForUser, + unread = it.workflowState == AlertWorkflowState.UNREAD, + htmlUrl = it.htmlUrl + ) + }.sortedByDescending { it.date }, + studentColor = 1 + ) + + assertEquals(expectedRefreshed, viewModel.uiState.value) + } + + @Test + fun `Dismiss alert`() = runTest { + val student = User(1L) + + val alerts = listOf( + Alert( + id = 1, + actionDate = Date.from( + Instant.parse("2024-01-03T00:00:00Z") + ), + title = "Alert 1", + workflowState = AlertWorkflowState.READ, + alertType = AlertType.ASSIGNMENT_MISSING, + htmlUrl = "https://example.com/alert1", + contextId = 1L, + contextType = "Course", + lockedForUser = false, + observerAlertThresholdId = 1L, + observerId = 1L, + userId = 2L + ) + ) + + coEvery { repository.getAlertsForStudent(student.id, any()) } returns alerts + coEvery { repository.updateAlertWorkflow(any(), any()) } returns mockk() + + createViewModel() + selectedStudentFlow.emit(student) + + val expected = AlertsUiState( + isLoading = false, + isError = false, + alerts = alerts.map { + AlertsItemUiState( + alertId = it.id, + title = it.title, + alertType = it.alertType, + date = it.actionDate, + observerAlertThreshold = null, + lockedForUser = it.lockedForUser, + unread = it.workflowState == AlertWorkflowState.UNREAD, + htmlUrl = it.htmlUrl + ) + }.sortedByDescending { it.date }, + studentColor = 1 + ) + + assertEquals(expected, viewModel.uiState.value) + + viewModel.handleAction(AlertsAction.DismissAlert(1L)) + assertEquals(emptyList(), viewModel.uiState.value.alerts) + + val events = mutableListOf() + + backgroundScope.launch(testDispatcher) { + viewModel.events.toList(events) + } + + assertEquals(R.string.alertDismissMessage, (events.last() as AlertsViewModelAction.ShowSnackbar).message) + assertEquals(R.string.alertDismissAction, (events.last() as AlertsViewModelAction.ShowSnackbar).action) + } + + @Test + fun `Dismiss error resets event`() = runTest { + val student = User(1L) + + val alerts = listOf( + Alert( + id = 1, + actionDate = Date.from( + Instant.parse("2024-01-03T00:00:00Z") + ), + title = "Alert 1", + workflowState = AlertWorkflowState.READ, + alertType = AlertType.ASSIGNMENT_MISSING, + htmlUrl = "https://example.com/alert1", + contextId = 1L, + contextType = "Course", + lockedForUser = false, + observerAlertThresholdId = 1L, + observerId = 1L, + userId = 2L + ) + ) + + coEvery { repository.getAlertsForStudent(student.id, any()) } returns alerts + coEvery { repository.updateAlertWorkflow(any(), any()) } throws Exception() + + createViewModel() + selectedStudentFlow.emit(student) + + val expected = AlertsUiState( + isLoading = false, + isError = false, + alerts = alerts.map { + AlertsItemUiState( + alertId = it.id, + title = it.title, + alertType = it.alertType, + date = it.actionDate, + observerAlertThreshold = null, + lockedForUser = it.lockedForUser, + unread = it.workflowState == AlertWorkflowState.UNREAD, + htmlUrl = it.htmlUrl + ) + }.sortedByDescending { it.date }, + studentColor = 1 + ) + + assertEquals(expected, viewModel.uiState.value) + + viewModel.handleAction(AlertsAction.DismissAlert(1L)) + + val events = mutableListOf() + + backgroundScope.launch(testDispatcher) { + viewModel.events.toList(events) + } + + assertEquals(R.string.alertDismissErrorMessage, (events.last() as AlertsViewModelAction.ShowSnackbar).message) + assertEquals(expected, viewModel.uiState.value) + } + + @Test + fun `Undo dismissal`() = runTest { + val student = User(1L) + + val alerts = listOf( + Alert( + id = 1, + actionDate = Date.from( + Instant.parse("2024-01-03T00:00:00Z") + ), + title = "Alert 1", + workflowState = AlertWorkflowState.READ, + alertType = AlertType.ASSIGNMENT_MISSING, + htmlUrl = "https://example.com/alert1", + contextId = 1L, + contextType = "Course", + lockedForUser = false, + observerAlertThresholdId = 1L, + observerId = 1L, + userId = 2L + ) + ) + + coEvery { repository.getAlertsForStudent(student.id, any()) } returns alerts + coEvery { repository.updateAlertWorkflow(any(), any()) } returns mockk() + + createViewModel() + selectedStudentFlow.emit(student) + + val expected = AlertsUiState( + isLoading = false, + isError = false, + alerts = alerts.map { + AlertsItemUiState( + alertId = it.id, + title = it.title, + alertType = it.alertType, + date = it.actionDate, + observerAlertThreshold = null, + lockedForUser = it.lockedForUser, + unread = it.workflowState == AlertWorkflowState.UNREAD, + htmlUrl = it.htmlUrl + ) + }.sortedByDescending { it.date }, + studentColor = 1 + ) + + assertEquals(expected, viewModel.uiState.value) + + viewModel.handleAction(AlertsAction.DismissAlert(1L)) + assertEquals(emptyList(), viewModel.uiState.value.alerts) + + val events = mutableListOf() + + backgroundScope.launch(testDispatcher) { + viewModel.events.toList(events) + } + + assertEquals(R.string.alertDismissMessage, (events.last() as AlertsViewModelAction.ShowSnackbar).message) + assertEquals(R.string.alertDismissAction, (events.last() as AlertsViewModelAction.ShowSnackbar).action) + + (events.last() as AlertsViewModelAction.ShowSnackbar).actionCallback?.invoke() + + assertEquals(expected, viewModel.uiState.value) + } + + @Test + fun `Undo does not reset event on error`() = runTest { + val student = User(1L) + + val alerts = listOf( + Alert( + id = 1, + actionDate = Date.from( + Instant.parse("2024-01-03T00:00:00Z") + ), + title = "Alert 1", + workflowState = AlertWorkflowState.READ, + alertType = AlertType.ASSIGNMENT_MISSING, + htmlUrl = "https://example.com/alert1", + contextId = 1L, + contextType = "Course", + lockedForUser = false, + observerAlertThresholdId = 1L, + observerId = 1L, + userId = 2L + ) + ) + + coEvery { repository.getAlertsForStudent(student.id, any()) } returns alerts + coEvery { repository.updateAlertWorkflow(any(), any()) } returns mockk() + + createViewModel() + selectedStudentFlow.emit(student) + + val expected = AlertsUiState( + isLoading = false, + isError = false, + alerts = alerts.map { + AlertsItemUiState( + alertId = it.id, + title = it.title, + alertType = it.alertType, + date = it.actionDate, + observerAlertThreshold = null, + lockedForUser = it.lockedForUser, + unread = it.workflowState == AlertWorkflowState.UNREAD, + htmlUrl = it.htmlUrl + ) + }.sortedByDescending { it.date }, + studentColor = 1 + ) + + assertEquals(expected, viewModel.uiState.value) + + viewModel.handleAction(AlertsAction.DismissAlert(1L)) + assertEquals(emptyList(), viewModel.uiState.value.alerts) + + val events = mutableListOf() + + backgroundScope.launch(testDispatcher) { + viewModel.events.toList(events) + } + + coEvery { repository.updateAlertWorkflow(any(), any()) } throws Exception() + (events.last() as AlertsViewModelAction.ShowSnackbar).actionCallback?.invoke() + + assertEquals(emptyList(), viewModel.uiState.value.alerts) + } + + @Test + fun `Navigate to URL`() = runTest { + val student = User(1L) + + val alerts = listOf( + Alert( + id = 1, + actionDate = Date.from( + Instant.parse("2024-01-03T00:00:00Z") + ), + title = "Alert 1", + workflowState = AlertWorkflowState.READ, + alertType = AlertType.ASSIGNMENT_MISSING, + htmlUrl = "https://example.com/alert1", + contextId = 1L, + contextType = "Course", + lockedForUser = false, + observerAlertThresholdId = 1L, + observerId = 1L, + userId = 2L + ) + ) + + coEvery { repository.getAlertsForStudent(student.id, any()) } returns alerts + + createViewModel() + selectedStudentFlow.emit(student) + + val expected = AlertsUiState( + isLoading = false, + isError = false, + alerts = alerts.map { + AlertsItemUiState( + alertId = it.id, + title = it.title, + alertType = it.alertType, + date = it.actionDate, + observerAlertThreshold = null, + lockedForUser = it.lockedForUser, + unread = it.workflowState == AlertWorkflowState.UNREAD, + htmlUrl = it.htmlUrl + ) + }.sortedByDescending { it.date }, + studentColor = 1 + ) + + assertEquals(expected, viewModel.uiState.value) + + viewModel.handleAction(AlertsAction.Navigate(1L, "https://example.com/alert1")) + + val events = mutableListOf() + + backgroundScope.launch(testDispatcher) { + viewModel.events.toList(events) + } + + assertEquals(AlertsViewModelAction.Navigate("https://example.com/alert1"), events.last()) + } + + @Test + fun `Navigation to alert marks it read`() = runTest { + val student = User(1L) + + val alerts = listOf( + Alert( + id = 1, + actionDate = Date.from( + Instant.parse("2024-01-03T00:00:00Z") + ), + title = "Alert 1", + workflowState = AlertWorkflowState.UNREAD, + alertType = AlertType.ASSIGNMENT_MISSING, + htmlUrl = "https://example.com/alert1", + contextId = 1L, + contextType = "Course", + lockedForUser = false, + observerAlertThresholdId = 1L, + observerId = 1L, + userId = 2L + ) + ) + + coEvery { repository.updateAlertWorkflow(1L, AlertWorkflowState.READ) } returns mockk() + coEvery { repository.getAlertsForStudent(student.id, any()) } returns alerts + + createViewModel() + selectedStudentFlow.emit(student) + + val expected = AlertsUiState( + isLoading = false, + isError = false, + alerts = alerts.map { + AlertsItemUiState( + alertId = it.id, + title = it.title, + alertType = it.alertType, + date = it.actionDate, + observerAlertThreshold = null, + lockedForUser = it.lockedForUser, + unread = false, + htmlUrl = it.htmlUrl + ) + }.sortedByDescending { it.date }, + studentColor = 1 + ) + + viewModel.handleAction(AlertsAction.Navigate(1L, "https://example.com/alert1")) + + val events = mutableListOf() + backgroundScope.launch(testDispatcher) { + viewModel.events.toList(events) + } + + assertEquals(AlertsViewModelAction.Navigate("https://example.com/alert1"), events.last()) + + assertEquals(expected, viewModel.uiState.value) + coVerify { + repository.updateAlertWorkflow(1L, AlertWorkflowState.READ) + } + } + + @Test + fun `If marking the alert read fails the alert will remain read until refresh`() = runTest { + val student = User(1L) + + val alerts = listOf( + Alert( + id = 1, + actionDate = Date.from( + Instant.parse("2024-01-03T00:00:00Z") + ), + title = "Alert 1", + workflowState = AlertWorkflowState.UNREAD, + alertType = AlertType.ASSIGNMENT_MISSING, + htmlUrl = "https://example.com/alert1", + contextId = 1L, + contextType = "Course", + lockedForUser = false, + observerAlertThresholdId = 1L, + observerId = 1L, + userId = 2L + ) + ) + + coEvery { repository.updateAlertWorkflow(1L, AlertWorkflowState.READ) } throws Exception() + coEvery { repository.getAlertsForStudent(student.id, any()) } returns alerts + + createViewModel() + selectedStudentFlow.emit(student) + + val expected = AlertsUiState( + isLoading = false, + isError = false, + alerts = alerts.map { + AlertsItemUiState( + alertId = it.id, + title = it.title, + alertType = it.alertType, + date = it.actionDate, + observerAlertThreshold = null, + lockedForUser = it.lockedForUser, + unread = false, + htmlUrl = it.htmlUrl + ) + }.sortedByDescending { it.date }, + studentColor = 1 + ) + + viewModel.handleAction(AlertsAction.Navigate(1L, "https://example.com/alert1")) + + val events = mutableListOf() + backgroundScope.launch(testDispatcher) { + viewModel.events.toList(events) + } + + assertEquals(AlertsViewModelAction.Navigate("https://example.com/alert1"), events.last()) + + assertEquals(expected, viewModel.uiState.value) + + } + + private fun createViewModel() { + viewModel = + AlertsViewModel(repository, colorKeeper, selectedStudentHolder, alertCountUpdater) + } +} \ No newline at end of file diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/courses/list/CoursesViewModelTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/courses/list/CoursesViewModelTest.kt index 557a2837b1..1081e88c20 100644 --- a/apps/parent/src/test/java/com/instructure/parentapp/features/courses/list/CoursesViewModelTest.kt +++ b/apps/parent/src/test/java/com/instructure/parentapp/features/courses/list/CoursesViewModelTest.kt @@ -32,7 +32,7 @@ import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -58,7 +58,7 @@ class CoursesViewModelTest { private val repository: CoursesRepository = mockk(relaxed = true) private val colorKeeper: ColorKeeper = mockk(relaxed = true) - private val selectedStudentFlow = MutableSharedFlow() + private val selectedStudentFlow = MutableStateFlow(null) private val selectedStudentHolder = TestSelectStudentHolder(selectedStudentFlow) private val courseGradeFormatter: CourseGradeFormatter = mockk(relaxed = true) diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/DashboardRepositoryTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/DashboardRepositoryTest.kt index a3393e9fac..12927db75a 100644 --- a/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/DashboardRepositoryTest.kt +++ b/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/DashboardRepositoryTest.kt @@ -27,8 +27,7 @@ import com.instructure.canvasapi2.utils.LinkHeaders import io.mockk.coEvery import io.mockk.mockk import kotlinx.coroutines.test.runTest -import org.junit.Assert -import org.junit.Assert.* +import org.junit.Assert.assertEquals import org.junit.Test diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/DashboardViewModelTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/DashboardViewModelTest.kt index 27509eff5c..d8ea2a5cdc 100644 --- a/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/DashboardViewModelTest.kt +++ b/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/DashboardViewModelTest.kt @@ -29,6 +29,7 @@ import com.instructure.loginapi.login.model.SignedInUser import com.instructure.loginapi.login.util.PreviousUsersUtils import com.instructure.pandautils.mvvm.ViewState import com.instructure.parentapp.R +import com.instructure.parentapp.features.alerts.list.AlertsRepository import com.instructure.parentapp.util.ParentPrefs import io.mockk.coEvery import io.mockk.coVerify @@ -41,8 +42,9 @@ import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.junit.After -import org.junit.Assert -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test @@ -60,12 +62,15 @@ class DashboardViewModelTest { private val context: Context = mockk(relaxed = true) private val repository: DashboardRepository = mockk(relaxed = true) + private val alertsRepository: AlertsRepository = mockk(relaxed = true) private val previousUsersUtils: PreviousUsersUtils = mockk(relaxed = true) private val apiPrefs: ApiPrefs = mockk(relaxed = true) private val parentPrefs: ParentPrefs = mockk(relaxed = true) private val selectedStudentHolder: SelectedStudentHolder = mockk(relaxed = true) private val inboxCountUpdaterFlow = MutableSharedFlow() private val inboxCountUpdater: InboxCountUpdater = TestInboxCountUpdater(inboxCountUpdaterFlow) + private val alertCountUpdaterFlow = MutableSharedFlow() + private val alertCountUpdater: AlertCountUpdater = TestAlertCountUpdater(alertCountUpdaterFlow) private lateinit var viewModel: DashboardViewModel @@ -92,7 +97,13 @@ class DashboardViewModelTest { avatarUrl = "avatar" ) - val expected = UserViewData(user.name, user.pronouns, user.shortName, user.avatarUrl, user.primaryEmail) + val expected = UserViewData( + user.name, + user.pronouns, + user.shortName, + user.avatarUrl, + user.primaryEmail + ) coEvery { apiPrefs.user } returns user createViewModel() @@ -206,15 +217,33 @@ class DashboardViewModelTest { assertEquals(1, viewModel.data.value.unreadCount) } + @Test + fun `Update alert count when the update alert count flow triggers`() = runTest { + val students = listOf(User(id = 1L), User(id = 2L)) + coEvery { repository.getStudents() } returns students + coEvery { alertsRepository.getUnreadAlertCount(1L) } returns 0 + + createViewModel() + + assertEquals(0, viewModel.data.value.alertCount) + + coEvery { alertsRepository.getUnreadAlertCount(1L) } returns 1 + alertCountUpdaterFlow.emit(true) + + assertEquals(1, viewModel.data.value.alertCount) + } + private fun createViewModel() { viewModel = DashboardViewModel( context = context, repository = repository, + alertsRepository = alertsRepository, previousUsersUtils = previousUsersUtils, apiPrefs = apiPrefs, parentPrefs = parentPrefs, selectedStudentHolder = selectedStudentHolder, - inboxCountUpdater = inboxCountUpdater + inboxCountUpdater = inboxCountUpdater, + alertCountUpdater = alertCountUpdater ) } } diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/TestAlertCountUpdater.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/TestAlertCountUpdater.kt new file mode 100644 index 0000000000..30deab629e --- /dev/null +++ b/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/TestAlertCountUpdater.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.parentapp.features.dashboard + +import kotlinx.coroutines.flow.MutableSharedFlow + +class TestAlertCountUpdater(override val shouldRefreshAlertCountFlow: MutableSharedFlow) : + AlertCountUpdater { + override suspend fun updateShouldRefreshAlertCount(shouldRefresh: Boolean) { + shouldRefreshAlertCountFlow.emit(shouldRefresh) + } +} \ No newline at end of file diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/TestSelectStudentHolder.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/TestSelectStudentHolder.kt index 6c34d1556e..183805bea0 100644 --- a/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/TestSelectStudentHolder.kt +++ b/apps/parent/src/test/java/com/instructure/parentapp/features/dashboard/TestSelectStudentHolder.kt @@ -18,11 +18,11 @@ package com.instructure.parentapp.features.dashboard import com.instructure.canvasapi2.models.User -import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow class TestSelectStudentHolder( - override val selectedStudentFlow: MutableSharedFlow + override val selectedStudentFlow: MutableStateFlow ) : SelectedStudentHolder { override suspend fun updateSelectedStudent(user: User) { selectedStudentFlow.emit(user) diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt index 93614030d9..e2a5005919 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/MockCanvas.kt @@ -24,6 +24,10 @@ import com.instructure.canvas.espresso.mockCanvas.utils.Randomizer import com.instructure.canvasapi2.apis.EnrollmentAPI import com.instructure.canvasapi2.models.Account import com.instructure.canvasapi2.models.AccountNotification +import com.instructure.canvasapi2.models.Alert +import com.instructure.canvasapi2.models.AlertThreshold +import com.instructure.canvasapi2.models.AlertType +import com.instructure.canvasapi2.models.AlertWorkflowState import com.instructure.canvasapi2.models.AnnotationMetadata import com.instructure.canvasapi2.models.AnnotationUrls import com.instructure.canvasapi2.models.Assignment @@ -293,6 +297,10 @@ class MockCanvas { val commentLibraryItems = mutableMapOf>() + /** Map of userId to alerts */ + var observerAlerts = mutableMapOf>() + val observerAlertThresholds = mutableMapOf>() + //region Convenience functionality /** A list of users with at least one Student enrollment */ @@ -2243,4 +2251,64 @@ fun MockCanvas.addPlannable(name: String, userId: Long, course: Course? = null, todos.add(todo) return todo +} + +fun MockCanvas.addObserverAlert( + observer: User, + student: User, + canvasContext: CanvasContext, + alertType: AlertType, + workflowState: AlertWorkflowState, + actionDate: Date, + htmlUrl: String?, + lockedForUser: Boolean, + threshold: String? = null, + observerAlertThresholdId: Long? = null +): Alert { + + val alerts = observerAlerts[student.id] ?: mutableListOf() + + val thresholdId: Long = observerAlertThresholdId ?: newItemId() + if (!observerAlertThresholds.containsKey(thresholdId)) { + addObserverAlertThreshold(thresholdId, alertType, observer, student, threshold) + } + + val alert = Alert( + id = newItemId(), + observerId = observer.id, + userId = student.id, + observerAlertThresholdId = thresholdId, + contextType = canvasContext.type.apiString, + contextId = canvasContext.id, + alertType = alertType, + workflowState = workflowState, + actionDate = actionDate, + title = Randomizer.randomAlertTitle(), + htmlUrl = htmlUrl, + lockedForUser = lockedForUser + ) + + val updatedList = alerts.toMutableList().apply { + add(alert) + } + + observerAlerts[student.id] = updatedList + + return alert +} + +fun MockCanvas.addObserverAlertThreshold(id: Long, alertType: AlertType, observer: User, student: User, threshold: String?) { + val thresholds = observerAlertThresholds[student.id]?.toMutableList() ?: mutableListOf() + + thresholds.add( + AlertThreshold( + id = id, + observerId = observer.id, + userId = student.id, + threshold = threshold, + alertType = alertType, + ) + ) + + observerAlertThresholds[student.id] = thresholds } \ No newline at end of file diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ObserverAlertsEndpoint.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ObserverAlertsEndpoint.kt new file mode 100644 index 0000000000..616df09ae3 --- /dev/null +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/ObserverAlertsEndpoint.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.instructure.canvas.espresso.mockCanvas.endpoints + +import com.instructure.canvas.espresso.mockCanvas.Endpoint +import com.instructure.canvas.espresso.mockCanvas.utils.LongId +import com.instructure.canvas.espresso.mockCanvas.utils.PathVars +import com.instructure.canvas.espresso.mockCanvas.utils.StringId +import com.instructure.canvas.espresso.mockCanvas.utils.successResponse +import com.instructure.canvas.espresso.mockCanvas.utils.unauthorizedResponse +import com.instructure.canvasapi2.models.Alert +import com.instructure.canvasapi2.models.AlertWorkflowState + +object ObserverAlertsEndpoint : Endpoint( + LongId(PathVars::studentId) to Endpoint( + StringId(PathVars::workflowState) to Endpoint( + response = { + PUT { + var updatedAlert: Alert? = null + data.observerAlerts = data.observerAlerts.mapValues { (_, alerts) -> + alerts.map { alert -> + if (alert.id == pathVars.studentId) { + updatedAlert = alert.copy(workflowState = AlertWorkflowState.valueOf(pathVars.workflowState.uppercase())) + updatedAlert!! + } else { + alert + } + } + }.toMutableMap() + updatedAlert?.let { + request.successResponse(it) + } ?: request.unauthorizedResponse() + } + } + ), + response = { + GET { + val alerts = data.observerAlerts[pathVars.studentId] ?: emptyList() + val filtered = alerts.filter { listOf(AlertWorkflowState.READ, AlertWorkflowState.UNREAD).contains(it.workflowState) } + request.successResponse(filtered) + } + } + ) +) \ No newline at end of file diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/UserEndpoints.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/UserEndpoints.kt index aa77522af7..36e6cf0041 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/UserEndpoints.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/endpoints/UserEndpoints.kt @@ -64,6 +64,16 @@ object UserListEndpoint : Endpoint( * - `enrollments` -> [UserEnrollmentEndpoint] */ object UserEndpoint : Endpoint( + Segment("observer_alerts") to ObserverAlertsEndpoint, + Segment("observer_alert_thresholds") to Endpoint( + response = { + GET { + val userId = request.url.queryParameter("student_id")?.toLong() ?: 0L + val response = data.observerAlertThresholds[userId] ?: emptyList() + request.successResponse(response) + } + } + ), Segment("profile") to UserProfileEndpoint, Segment("colors") to UserColorsEndpoint, Segment("pandata_events_token") to endpoint { diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/PathUtils.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/PathUtils.kt index fb05dc4328..8e3d7d0170 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/PathUtils.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/PathUtils.kt @@ -51,6 +51,8 @@ class PathVars { var progressId: Long by map var plannerNoteId: Long by map var eventId: Long by map + var studentId: Long by map + var workflowState: String by map } /** diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/Randomizer.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/Randomizer.kt index af92aaab27..a57a41f0db 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/Randomizer.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/mockCanvas/utils/Randomizer.kt @@ -90,6 +90,8 @@ object Randomizer { /** Creates random name for an assignment */ fun randomAssignmentName(): String = "${faker.starTrek().character()} ${UUID.randomUUID()}" + fun randomAlertTitle(): String = faker.starTrek().location() + } /** Represents a fake user name */ diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ObserverApi.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ObserverApi.kt new file mode 100644 index 0000000000..2bbb430e5b --- /dev/null +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/ObserverApi.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.instructure.canvasapi2.apis + +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.Alert +import com.instructure.canvasapi2.models.AlertThreshold +import com.instructure.canvasapi2.utils.DataResult +import retrofit2.http.GET +import retrofit2.http.PUT +import retrofit2.http.Path +import retrofit2.http.Query +import retrofit2.http.Tag +import retrofit2.http.Url + +interface ObserverApi { + + @GET("users/self/observer_alerts/{studentId}") + suspend fun getObserverAlerts(@Path("studentId") studentId: Long, @Tag restParams: RestParams): DataResult> + + @GET + suspend fun getNextPageObserverAlerts(@Url nextUrl: String, @Tag restParams: RestParams): DataResult> + + @PUT("users/self/observer_alerts/{alertId}/{workflowState}") + suspend fun updateAlertWorkflow(@Path("alertId") alertId: Long, @Path("workflowState") workflowState: String, @Tag restParams: RestParams): DataResult + + @GET("users/self/observer_alert_thresholds") + suspend fun getObserverAlertThresholds(@Query("student_id") studentId: Long, @Tag restParams: RestParams): DataResult> +} \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UnreadCountAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UnreadCountAPI.kt index 9acfa5b720..39df056dd7 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UnreadCountAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UnreadCountAPI.kt @@ -26,6 +26,9 @@ object UnreadCountAPI { @GET("users/self/observer_alerts/unread_count") fun getUnreadAlertCount(@Query("student_id") studentId: Long): Call + + @GET("users/self/observer_alerts/unread_count") + suspend fun getUnreadAlertCount(@Query("student_id") studentId: Long, @Tag params: RestParams): DataResult } fun getUnreadConversationCount(adapter: RestBuilder, params: RestParams, callback: StatusCallback) { diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/ApiModule.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/ApiModule.kt index d9821350e8..c1d2edaa0c 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/ApiModule.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/di/ApiModule.kt @@ -262,6 +262,11 @@ class ApiModule { return RestBuilder().build(ThemeAPI.ThemeInterface::class.java, RestParams()) } + @Provides + fun provideObserverApi(): ObserverApi { + return RestBuilder().build(ObserverApi::class.java, RestParams()) + } + @Provides fun provideUnreadCountApi(): UnreadCountAPI.UnreadCountsInterface { return RestBuilder().build(UnreadCountAPI.UnreadCountsInterface::class.java, RestParams()) diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Alert.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Alert.kt new file mode 100644 index 0000000000..67e0bb174f --- /dev/null +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/Alert.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.instructure.canvasapi2.models + +import com.google.gson.annotations.SerializedName +import java.util.Date + +data class Alert( + val id: Long, + @SerializedName("observer_alert_threshold_id") + val observerAlertThresholdId: Long, + @SerializedName("context_type") + val contextType: String, + @SerializedName("context_id") + val contextId: Long, + @SerializedName("alert_type") + val alertType: AlertType, + @SerializedName("workflow_state") + val workflowState: AlertWorkflowState, + @SerializedName("action_date") + val actionDate: Date?, + val title: String, + @SerializedName("user_id") + val userId: Long, + @SerializedName("observer_id") + val observerId: Long, + @SerializedName("html_url") + val htmlUrl: String?, + @SerializedName("locked_for_user") + val lockedForUser: Boolean +) { + fun getCourseId(): Long? { + return when(alertType) { + AlertType.COURSE_GRADE_LOW, AlertType.COURSE_GRADE_HIGH, AlertType.COURSE_ANNOUNCEMENT -> contextId + AlertType.ASSIGNMENT_GRADE_HIGH, AlertType.ASSIGNMENT_GRADE_LOW, AlertType.ASSIGNMENT_MISSING -> { + Regex("courses/(\\d+)/").find(htmlUrl.orEmpty())?.groupValues?.get(1)?.toLong() + } + else -> null + } + } + + fun isQuantitativeRestrictionApplies(): Boolean { + return when (alertType) { + AlertType.COURSE_GRADE_LOW, AlertType.COURSE_GRADE_HIGH, AlertType.ASSIGNMENT_GRADE_HIGH, AlertType.ASSIGNMENT_GRADE_LOW -> true + else -> false + } + } +} + +enum class AlertType { + @SerializedName("assignment_missing") + ASSIGNMENT_MISSING, + @SerializedName("assignment_grade_high") + ASSIGNMENT_GRADE_HIGH, + @SerializedName("assignment_grade_low") + ASSIGNMENT_GRADE_LOW, + @SerializedName("course_grade_high") + COURSE_GRADE_HIGH, + @SerializedName("course_grade_low") + COURSE_GRADE_LOW, + @SerializedName("course_announcement") + COURSE_ANNOUNCEMENT, + @SerializedName("institution_announcement") + INSTITUTION_ANNOUNCEMENT; + + fun isAlertInfo() = listOf(INSTITUTION_ANNOUNCEMENT, COURSE_ANNOUNCEMENT).contains(this) + + fun isAlertPositive() = listOf(COURSE_GRADE_HIGH, ASSIGNMENT_GRADE_HIGH).contains(this) + + fun isAlertNegative() = listOf(ASSIGNMENT_MISSING, ASSIGNMENT_GRADE_LOW, COURSE_GRADE_LOW).contains(this) +} + +enum class AlertWorkflowState { + @SerializedName("unread") + UNREAD, + @SerializedName("read") + READ, + @SerializedName("deleted") + DELETED, + @SerializedName("dismissed") + DISMISSED +} \ No newline at end of file diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/AlertThreshold.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/AlertThreshold.kt new file mode 100644 index 0000000000..90c6e2af33 --- /dev/null +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/AlertThreshold.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.instructure.canvasapi2.models + +import com.google.gson.annotations.SerializedName + +data class AlertThreshold( + val id: Long, + @SerializedName("alert_type") + val alertType: AlertType, + val threshold: String?, + @SerializedName("user_id") + val userId: Long, + @SerializedName("observer_id") + val observerId: Long +) \ No newline at end of file diff --git a/libs/pandares/src/main/res/values/strings.xml b/libs/pandares/src/main/res/values/strings.xml index 21ab35b434..89b4ce4594 100644 --- a/libs/pandares/src/main/res/values/strings.xml +++ b/libs/pandares/src/main/res/values/strings.xml @@ -1762,4 +1762,20 @@ Canvas Teacher 99+ Open navigation drawer, %s unread messages + No Alerts + There\'s nothing to be notified of yet. + There was an error loading your student’s alerts. + Assignment missing + Assignment Grade Above %s + Assignment Grade Below %s + Course Grade Above %s + Course Grade Below %s + Course Announcement + Institution Announcement + Delete Alert + %s at %s + Alert dismissed + Undo + Failed to undo alert dismissal + Failed to dismiss alert diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ComposeExtensions.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ComposeExtensions.kt new file mode 100644 index 0000000000..dda0ee295d --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ComposeExtensions.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package com.instructure.pandautils.utils + +import androidx.compose.ui.semantics.SemanticsPropertyKey +import androidx.compose.ui.semantics.SemanticsPropertyReceiver + +val DrawableId = SemanticsPropertyKey("DrawableResId") +var SemanticsPropertyReceiver.drawableId by DrawableId \ No newline at end of file From c8d7e05531b4ae5c3a393c9198a3d1a275bc3bf3 Mon Sep 17 00:00:00 2001 From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com> Date: Fri, 26 Jul 2024 08:53:09 +0200 Subject: [PATCH 15/50] [MBL-17685][Student][Teacher] Remove Hybrid discussion feature flag (#2501) refs: MBL-17685 affects: Student, Teacher release note: none * Removed feature flags. * Fixed interaction tests. * Fixed interaction tests. * Fixed tests. * Removed old discussions code from the Teacher app. * Fixed student build. --- .../AnnouncementInteractionTest.kt | 4 + .../interaction/DiscussionsInteractionTest.kt | 18 + .../interaction/GroupLinksInteractionTest.kt | 4 +- .../ui/interaction/HomeroomInteractionTest.kt | 3 +- .../ui/interaction/ModuleInteractionTest.kt | 2 +- .../StudentCalendarInteractionTest.kt | 7 +- .../DiscussionRouteHelperStudentRepository.kt | 10 +- .../CourseModuleProgressionFragment.kt | 4 - .../features/modules/util/ModuleUtility.kt | 3 +- ...cussionRouteHelperStudentRepositoryTest.kt | 24 +- .../student/test/util/ModuleUtilityTest.kt | 87 +- .../teacher/ui/TeacherCalendarPageTest.kt | 7 +- .../ui/pages/NativeDiscussionsDetailsPage.kt | 101 --- .../teacher/ui/utils/TeacherTest.kt | 2 - .../instructure/teacher/events/BusEvents.kt | 6 - .../DiscussionsDetailsPresenterFactory.kt | 33 - .../factory/DiscussionsReplyFactory.kt | 32 - .../discussion/DiscussionsDetailsFragment.kt | 843 ------------------ .../DiscussionRouteHelperTeacherRepository.kt | 8 +- .../routing/TeacherDiscussionRouter.kt | 13 +- .../progression/ModuleProgressionFragment.kt | 15 +- .../progression/ModuleProgressionViewData.kt | 2 +- .../progression/ModuleProgressionViewModel.kt | 8 +- .../fragments/DiscussionsListFragment.kt | 4 +- .../fragments/DiscussionsReplyFragment.kt | 202 ----- .../presenters/CreateDiscussionPresenter.kt | 4 +- .../CreateOrEditAnnouncementPresenter.kt | 6 +- .../presenters/DiscussionsDetailsPresenter.kt | 319 ------- .../presenters/DiscussionsReplyPresenter.kt | 110 --- .../teacher/router/RouteMatcher.kt | 27 - .../teacher/router/RouteResolver.kt | 25 - .../viewinterface/DiscussionsDetailsView.kt | 35 - .../viewinterface/DiscussionsReplyView.kt | 26 - .../layout/fragment_discussions_details.xml | 532 ----------- .../res/layout/fragment_discussions_reply.xml | 53 -- ...cussionRouteHelperTeacherRepositoryTest.kt | 6 +- .../ModuleProgressionViewModelTest.kt | 5 +- .../interaction/CalendarInteractionTest.kt | 2 + .../canvasapi2/apis/DiscussionAPI.kt | 49 +- .../canvasapi2/managers/DiscussionManager.kt | 67 -- .../pandautils/di/DiscussionModule.kt | 6 +- .../router/DiscussionRouteHelper.kt | 4 +- .../DiscussionRouteHelperNetworkDataSource.kt | 25 +- .../router/DiscussionRouteHelperRepository.kt | 2 +- .../router/DiscussionRouterViewModel.kt | 16 +- .../pandautils/utils/FeatureFlagProvider.kt | 4 - ...cussionRouteHelperNetworkDataSourceTest.kt | 43 +- .../router/DiscussionRouterViewModelTest.kt | 10 +- 48 files changed, 145 insertions(+), 2673 deletions(-) delete mode 100644 apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/NativeDiscussionsDetailsPage.kt delete mode 100644 apps/teacher/src/main/java/com/instructure/teacher/factory/DiscussionsDetailsPresenterFactory.kt delete mode 100644 apps/teacher/src/main/java/com/instructure/teacher/factory/DiscussionsReplyFactory.kt delete mode 100644 apps/teacher/src/main/java/com/instructure/teacher/features/discussion/DiscussionsDetailsFragment.kt delete mode 100644 apps/teacher/src/main/java/com/instructure/teacher/fragments/DiscussionsReplyFragment.kt delete mode 100644 apps/teacher/src/main/java/com/instructure/teacher/presenters/DiscussionsDetailsPresenter.kt delete mode 100644 apps/teacher/src/main/java/com/instructure/teacher/presenters/DiscussionsReplyPresenter.kt delete mode 100644 apps/teacher/src/main/java/com/instructure/teacher/viewinterface/DiscussionsDetailsView.kt delete mode 100644 apps/teacher/src/main/java/com/instructure/teacher/viewinterface/DiscussionsReplyView.kt delete mode 100644 apps/teacher/src/main/res/layout/fragment_discussions_details.xml delete mode 100644 apps/teacher/src/main/res/layout/fragment_discussions_reply.xml diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AnnouncementInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AnnouncementInteractionTest.kt index 31bbf68007..582ece4af2 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AnnouncementInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/AnnouncementInteractionTest.kt @@ -19,6 +19,7 @@ import androidx.test.espresso.Espresso import androidx.test.espresso.web.webdriver.Locator import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority +import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.mockCanvas.MockCanvas @@ -53,6 +54,7 @@ class AnnouncementInteractionTest : StudentTest() { // (This kind of seems like more of a test of the mocked endpoint, but we'll go with it.) @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION) + @Stub("This can only test the old discussions, will be modified later to test the old discussions in offline mode") fun testAnnouncement_replyToSectionSpecificAnnouncement() { val data = getToCourse(createSections = true) @@ -91,6 +93,7 @@ class AnnouncementInteractionTest : StudentTest() { // User can preview an announcement attachment @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION) + @Stub("This can only test the old discussions, will be modified later to test the old discussions in offline mode") fun testAnnouncement_previewAttachment() { val data = getToCourse() @@ -133,6 +136,7 @@ class AnnouncementInteractionTest : StudentTest() { // View/reply to an announcement @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.ANNOUNCEMENTS, TestCategory.INTERACTION) + @Stub("This can only test the old discussions, will be modified later to test the old discussions in offline mode") fun testAnnouncement_reply() { val data = getToCourse() diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt index facb557460..7aa0d73eb2 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/DiscussionsInteractionTest.kt @@ -21,6 +21,7 @@ import androidx.test.espresso.Espresso import androidx.test.espresso.web.webdriver.Locator import com.instructure.canvas.espresso.FeatureCategory import com.instructure.canvas.espresso.Priority +import com.instructure.canvas.espresso.Stub import com.instructure.canvas.espresso.TestCategory import com.instructure.canvas.espresso.TestMetaData import com.instructure.canvas.espresso.mockCanvas.MockCanvas @@ -51,6 +52,7 @@ class DiscussionsInteractionTest : StudentTest() { // Verify that a discussion header shows up properly after discussion creation @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION) + @Stub("This can only test the old discussions, will be modified later to test the old discussions in offline mode") fun testDiscussionCreate_base() { val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true) @@ -70,6 +72,7 @@ class DiscussionsInteractionTest : StudentTest() { // so the attachment is done behind the scenes, after the fact. @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION) + @Stub("This can only test the old discussions, will be modified later to test the old discussions in offline mode") fun testDiscussionCreate_withAttachment() { val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true) val course = data.courses.values.first() @@ -125,6 +128,7 @@ class DiscussionsInteractionTest : StudentTest() { // Tests that links to other Canvas content routes properly @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION) + @Stub("This can only test the old discussions, will be modified later to test the old discussions in offline mode") fun testDiscussion_linksRouteInApp() { val data = getToCourse(studentCount = 2, courseCount = 2, enableDiscussionTopicCreation = true) val course1 = data.courses.values.first() @@ -153,6 +157,7 @@ class DiscussionsInteractionTest : StudentTest() { // Replies automatically get marked as read as the user scrolls through the list @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION) + @Stub("This can only test the old discussions, will be modified later to test the old discussions in offline mode") fun testDiscussion_postsGetMarkedAsRead() { val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true) val course = data.courses.values.first() @@ -192,6 +197,7 @@ class DiscussionsInteractionTest : StudentTest() { // NOTE: Very similar to testDiscussionCreate_withAttachment @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION) + @Stub("This can only test the old discussions, will be modified later to test the old discussions in offline mode") fun testDiscussion_previewAttachment() { val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true) @@ -234,6 +240,7 @@ class DiscussionsInteractionTest : StudentTest() { // Tests that users can like entries and the correct like count is displayed, if the liking is enabled @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION) + @Stub("This can only test the old discussions, will be modified later to test the old discussions in offline mode") fun testDiscussionLikePost_base() { val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true) @@ -275,6 +282,7 @@ class DiscussionsInteractionTest : StudentTest() { // Tests that like count is shown if only graders can like @Test @TestMetaData(Priority.IMPORTANT, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION) + @Stub("This can only test the old discussions, will be modified later to test the old discussions in offline mode") fun testDiscussionLikes_whenOnlyGradersCanRate() { val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true) val course = data.courses.values.first() @@ -341,6 +349,7 @@ class DiscussionsInteractionTest : StudentTest() { // Test basic discussion view @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION) + @Stub("This can only test the old discussions, will be modified later to test the old discussions in offline mode") fun testDiscussionView_base() { val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true) val course1 = data.courses.values.first() @@ -361,6 +370,7 @@ class DiscussionsInteractionTest : StudentTest() { // Test that you can reply to a discussion (if enabled) @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION) + @Stub("This can only test the old discussions, will be modified later to test the old discussions in offline mode") fun testDiscussionView_replies() { val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true) val course1 = data.courses.values.first() @@ -388,6 +398,7 @@ class DiscussionsInteractionTest : StudentTest() { // Test that replies are not possible when they are not enabled @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION) + @Stub("This can only test the old discussions, will be modified later to test the old discussions in offline mode") fun testDiscussionView_repliesHiddenWhenNotPermitted() { val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true) val course1 = data.courses.values.first() @@ -409,6 +420,7 @@ class DiscussionsInteractionTest : StudentTest() { // Test that a reply is displayed properly @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION) + @Stub("This can only test the old discussions, will be modified later to test the old discussions in offline mode") fun testDiscussionReply_base() { val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true) val course1 = data.courses.values.first() @@ -437,6 +449,7 @@ class DiscussionsInteractionTest : StudentTest() { // so we add the attachments programmatically. @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION) + @Stub("This can only test the old discussions, will be modified later to test the old discussions in offline mode") fun testDiscussionReply_withAttachment() { val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true) val course1 = data.courses.values.first() @@ -492,6 +505,7 @@ class DiscussionsInteractionTest : StudentTest() { // Tests that we can make a threaded reply to a reply @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION) + @Stub("This can only test the old discussions, will be modified later to test the old discussions in offline mode") fun testDiscussionReply_threaded() { val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true) val course1 = data.courses.values.first() @@ -529,6 +543,7 @@ class DiscussionsInteractionTest : StudentTest() { // so we add the attachments programmatically. @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION) + @Stub("This can only test the old discussions, will be modified later to test the old discussions in offline mode") fun testDiscussionReply_threadedWithAttachment() { val data = getToCourse(studentCount = 1, courseCount = 1, enableDiscussionTopicCreation = true) val course1 = data.courses.values.first() @@ -593,6 +608,7 @@ class DiscussionsInteractionTest : StudentTest() { // Tests a discussion with a linked assignment. @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION) + @Stub("This can only test the old discussions, will be modified later to test the old discussions in offline mode") fun testDiscussion_linkedAssignment() { val data = MockCanvas.init(teacherCount = 1, studentCount = 1, courseCount = 1, favoriteCourseCount = 1) @@ -634,6 +650,7 @@ class DiscussionsInteractionTest : StudentTest() { // Tests a discussion with a linked assignment, show possible points if not restricted @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION) + @Stub("This can only test the old discussions, will be modified later to test the old discussions in offline mode") fun testDiscussion_showPointsIfNotRestricted() { val data = MockCanvas.init(teacherCount = 1, studentCount = 1, courseCount = 1, favoriteCourseCount = 1) @@ -679,6 +696,7 @@ class DiscussionsInteractionTest : StudentTest() { // Tests a discussion with a linked assignment, hide possible points if restricted @Test @TestMetaData(Priority.MANDATORY, FeatureCategory.DISCUSSIONS, TestCategory.INTERACTION) + @Stub("This can only test the old discussions, will be modified later to test the old discussions in offline mode") fun testDiscussion_hidePointsIfRestricted() { val data = MockCanvas.init(teacherCount = 1, studentCount = 1, courseCount = 1, favoriteCourseCount = 1) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GroupLinksInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GroupLinksInteractionTest.kt index 07eb0b0b5e..16579a02bd 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GroupLinksInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/GroupLinksInteractionTest.kt @@ -134,7 +134,7 @@ class GroupLinksInteractionTest : StudentTest() { dashboardPage.selectGroup(group) courseBrowserPage.selectAnnouncements() discussionListPage.selectTopic(announcement.title!!) - nativeDiscussionDetailsPage.assertTopicInfoShowing(announcement) + discussionDetailsPage.assertToolbarDiscussionTitle(announcement.title!!) } @@ -156,7 +156,7 @@ class GroupLinksInteractionTest : StudentTest() { dashboardPage.selectGroup(group) courseBrowserPage.selectDiscussions() discussionListPage.selectTopic(discussion.title!!) - nativeDiscussionDetailsPage.assertTopicInfoShowing(discussion) + discussionDetailsPage.assertToolbarDiscussionTitle(discussion.title!!) } // Link to group discussion list opens list - eg: "/groups/:id/discussion_topics" diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/HomeroomInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/HomeroomInteractionTest.kt index edb474d9ef..8fe96b8638 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/HomeroomInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/HomeroomInteractionTest.kt @@ -198,8 +198,7 @@ class HomeroomInteractionTest : StudentTest() { homeroomPage.openCourseAnnouncement(courseAnnouncement.title!!) - nativeDiscussionDetailsPage.assertPageObjects() - nativeDiscussionDetailsPage.assertTitleText(courseAnnouncement.title!!) + discussionDetailsPage.assertToolbarDiscussionTitle(courseAnnouncement.title!!) } @Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt index c89e02f3b2..5564b86d71 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/ModuleInteractionTest.kt @@ -126,7 +126,7 @@ class ModuleInteractionTest : StudentTest() { modulesPage.assertModuleDisplayed(module) modulesPage.assertModuleItemDisplayed(module, topicHeader!!.title!!) modulesPage.clickModuleItem(module, topicHeader!!.title!!) - nativeDiscussionDetailsPage.assertTopicInfoShowing(topicHeader!!) + discussionDetailsPage.assertToolbarDiscussionTitle(topicHeader!!.title!!) } @Test diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCalendarInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCalendarInteractionTest.kt index ef56e7da4c..b834d48deb 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCalendarInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/StudentCalendarInteractionTest.kt @@ -21,10 +21,11 @@ import com.instructure.canvas.espresso.mockCanvas.init import com.instructure.canvasapi2.models.User import com.instructure.espresso.ModuleItemInteractions import com.instructure.student.BuildConfig +import com.instructure.student.R import com.instructure.student.activity.LoginActivity import com.instructure.student.ui.pages.AssignmentDetailsPage import com.instructure.student.ui.pages.DashboardPage -import com.instructure.student.ui.pages.offline.NativeDiscussionDetailsPage +import com.instructure.student.ui.pages.DiscussionDetailsPage import com.instructure.student.ui.utils.StudentActivityTestRule import com.instructure.student.ui.utils.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @@ -38,7 +39,7 @@ class StudentCalendarInteractionTest : CalendarInteractionTest() { private val dashboardPage = DashboardPage() private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions()) - private val nativeDiscussionDetailsPage = NativeDiscussionDetailsPage(ModuleItemInteractions()) + private val discussionDetailsPage = DiscussionDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next_item, R.id.prev_item)) override fun goToCalendar(data: MockCanvas) { val student = data.students[0] @@ -68,6 +69,6 @@ class StudentCalendarInteractionTest : CalendarInteractionTest() { } override fun assertDiscussionDetailsTitle(title: String) { - nativeDiscussionDetailsPage.assertTitleText(title) + discussionDetailsPage.assertToolbarDiscussionTitle(title) } } \ No newline at end of file diff --git a/apps/student/src/main/java/com/instructure/student/features/discussion/routing/DiscussionRouteHelperStudentRepository.kt b/apps/student/src/main/java/com/instructure/student/features/discussion/routing/DiscussionRouteHelperStudentRepository.kt index 586dfa2ec2..c9397f04ad 100644 --- a/apps/student/src/main/java/com/instructure/student/features/discussion/routing/DiscussionRouteHelperStudentRepository.kt +++ b/apps/student/src/main/java/com/instructure/student/features/discussion/routing/DiscussionRouteHelperStudentRepository.kt @@ -13,16 +13,12 @@ import com.instructure.pandautils.utils.NetworkStateProvider class DiscussionRouteHelperStudentRepository( localDataSource: DiscussionRouteHelperLocalDataSource, - private val networkDataSource: DiscussionRouteHelperNetworkDataSource, + networkDataSource: DiscussionRouteHelperNetworkDataSource, networkStateProvider: NetworkStateProvider, featureFlagProvider: FeatureFlagProvider ) : DiscussionRouteHelperRepository, Repository(localDataSource, networkDataSource, networkStateProvider, featureFlagProvider) { - override suspend fun getEnabledFeaturesForCourse( - canvasContext: CanvasContext, - forceNetwork: Boolean - ): Boolean { - return networkDataSource.getEnabledFeaturesForCourse(canvasContext, forceNetwork) - } + + override suspend fun shouldShowDiscussionRedesign(): Boolean = isOnline() || !isOfflineEnabled() override suspend fun getDiscussionTopicHeader( canvasContext: CanvasContext, diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/progression/CourseModuleProgressionFragment.kt b/apps/student/src/main/java/com/instructure/student/features/modules/progression/CourseModuleProgressionFragment.kt index 7338f5d9a9..3b44d2bd71 100644 --- a/apps/student/src/main/java/com/instructure/student/features/modules/progression/CourseModuleProgressionFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/progression/CourseModuleProgressionFragment.kt @@ -105,8 +105,6 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { // This will keep track of where we need to be. private var currentPos = 0 - private var isDiscussionRedesignEnabled = false - private var snycedTabs = emptySet() private var syncedFileIds = emptyList() private var isOfflineEnabled = false @@ -137,7 +135,6 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { isOfflineEnabled = repository.isOfflineEnabled() snycedTabs = repository.getSyncedTabs(canvasContext.id) syncedFileIds = repository.getSyncedFileIds(canvasContext.id) - isDiscussionRedesignEnabled = discussionRouteHelper.isDiscussionRedesignEnabled(canvasContext) loadModuleProgression(savedInstanceState) } } @@ -643,7 +640,6 @@ class CourseModuleProgressionFragment : ParentFragment(), Bookmarkable { moduleItem!!, canvasContext as Course, modules[groupPos], - isDiscussionRedesignEnabled, navigatedFromModules, repository.isOnline() || !isOfflineEnabled, // If the offline feature is disabled we always use the online behavior snycedTabs, diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/util/ModuleUtility.kt b/apps/student/src/main/java/com/instructure/student/features/modules/util/ModuleUtility.kt index fdea8d6c99..aed581b1e8 100644 --- a/apps/student/src/main/java/com/instructure/student/features/modules/util/ModuleUtility.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/util/ModuleUtility.kt @@ -49,7 +49,6 @@ object ModuleUtility { item: ModuleItem, course: Course, moduleObject: ModuleObject?, - isDiscussionRedesignEnabled: Boolean, navigatedFromModules: Boolean, isOnline: Boolean, syncedTabs: Set, @@ -64,7 +63,7 @@ object ModuleUtility { } "Discussion" -> { createFragmentWithOfflineCheck(isOnline, course, item, syncedTabs, context, setOf(Tab.DISCUSSIONS_ID)) { - if (isDiscussionRedesignEnabled && isOnline) { + if (isOnline) { DiscussionDetailsWebViewFragment.newInstance(getDiscussionRedesignRoute(item, course)) } else { DiscussionDetailsFragment.newInstance(getDiscussionRoute(item, course)) diff --git a/apps/student/src/test/java/com/instructure/student/features/discussion/routing/DiscussionRouteHelperStudentRepositoryTest.kt b/apps/student/src/test/java/com/instructure/student/features/discussion/routing/DiscussionRouteHelperStudentRepositoryTest.kt index 71e3db5f29..5dc297d392 100644 --- a/apps/student/src/test/java/com/instructure/student/features/discussion/routing/DiscussionRouteHelperStudentRepositoryTest.kt +++ b/apps/student/src/test/java/com/instructure/student/features/discussion/routing/DiscussionRouteHelperStudentRepositoryTest.kt @@ -4,7 +4,6 @@ import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.models.Group import com.instructure.pandautils.features.discussion.router.DiscussionRouteHelperLocalDataSource import com.instructure.pandautils.features.discussion.router.DiscussionRouteHelperNetworkDataSource -import com.instructure.pandautils.utils.FEATURE_FLAG_OFFLINE import com.instructure.pandautils.utils.FeatureFlagProvider import com.instructure.pandautils.utils.NetworkStateProvider import io.mockk.coEvery @@ -31,25 +30,36 @@ class DiscussionRouteHelperStudentRepositoryTest { } @Test - fun `Call getEnabledFeaturesForCourse function when device is online`() = runTest { + fun `Show discussion redesign when device is online`() = runTest { val expected = true coEvery { networkStateProvider.isOnline() } returns true - coEvery { networkDataSource.getEnabledFeaturesForCourse(any(), any()) } returns expected - val result = repository.getEnabledFeaturesForCourse(mockk(), false) + val result = repository.shouldShowDiscussionRedesign() assertEquals(expected, result) } @Test - fun `Call getEnabledFeaturesForCourse function when device is offline`() = runTest { + fun `Show discussion redesign when device is offline and offline is disabled`() = runTest { val expected = true coEvery { networkStateProvider.isOnline() } returns false - coEvery { networkDataSource.getEnabledFeaturesForCourse(any(), any()) } returns expected + coEvery { featureFlagProvider.offlineEnabled() } returns false - val result = repository.getEnabledFeaturesForCourse(mockk(), false) + val result = repository.shouldShowDiscussionRedesign() + + assertEquals(expected, result) + } + + @Test + fun `Dont show discussion redesign when device is offline and offline is enabled`() = runTest { + val expected = false + + coEvery { networkStateProvider.isOnline() } returns false + coEvery { featureFlagProvider.offlineEnabled() } returns true + + val result = repository.shouldShowDiscussionRedesign() assertEquals(expected, result) } diff --git a/apps/student/src/test/java/com/instructure/student/test/util/ModuleUtilityTest.kt b/apps/student/src/test/java/com/instructure/student/test/util/ModuleUtilityTest.kt index 25e4cae7c8..7381916a36 100644 --- a/apps/student/src/test/java/com/instructure/student/test/util/ModuleUtilityTest.kt +++ b/apps/student/src/test/java/com/instructure/student/test/util/ModuleUtilityTest.kt @@ -25,6 +25,7 @@ import com.instructure.canvasapi2.models.Course import com.instructure.canvasapi2.models.ModuleItem import com.instructure.canvasapi2.models.ModuleObject import com.instructure.canvasapi2.models.Tab +import com.instructure.pandautils.features.discussion.details.DiscussionDetailsWebViewFragment import com.instructure.student.features.assignments.details.AssignmentDetailsFragment import com.instructure.student.features.discussion.details.DiscussionDetailsFragment import com.instructure.student.features.files.details.FileDetailsFragment @@ -69,9 +70,9 @@ class ModuleUtilityTest : TestCase() { var parentFragment = callGetFragment(moduleItem, course, moduleObject) - TestCase.assertNotNull(parentFragment) - TestCase.assertEquals(FileDetailsFragment::class.java, parentFragment!!.javaClass) - TestCase.assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) + assertNotNull(parentFragment) + assertEquals(FileDetailsFragment::class.java, parentFragment!!.javaClass) + assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) // Test module object is null moduleObject = null @@ -80,9 +81,9 @@ class ModuleUtilityTest : TestCase() { expectedBundle.putString(Const.FILE_URL, expectedUrl) expectedBundle.putInt(Const.FILE_ID, 0) parentFragment = callGetFragment(moduleItem, course, moduleObject) - TestCase.assertNotNull(parentFragment) - TestCase.assertEquals(FileDetailsFragment::class.java, parentFragment!!.javaClass) - TestCase.assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) + assertNotNull(parentFragment) + assertEquals(FileDetailsFragment::class.java, parentFragment!!.javaClass) + assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) } @@ -102,8 +103,8 @@ class ModuleUtilityTest : TestCase() { val course = Course() val filDetailsFragment = callGetFragment(moduleItem, course, moduleObject, isOnline = false) - TestCase.assertNotNull(filDetailsFragment) - TestCase.assertEquals(NotAvailableOfflineFragment::class.java, filDetailsFragment!!.javaClass) + assertNotNull(filDetailsFragment) + assertEquals(NotAvailableOfflineFragment::class.java, filDetailsFragment!!.javaClass) } @Test @@ -123,9 +124,9 @@ class ModuleUtilityTest : TestCase() { expectedBundle.putBoolean(PageDetailsFragment.NAVIGATED_FROM_MODULES, false) val parentFragment = callGetFragment(moduleItem, course, null) - TestCase.assertNotNull(parentFragment) - TestCase.assertEquals(PageDetailsFragment::class.java, parentFragment!!.javaClass) - TestCase.assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) + assertNotNull(parentFragment) + assertEquals(PageDetailsFragment::class.java, parentFragment!!.javaClass) + assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) } @Test @@ -143,9 +144,9 @@ class ModuleUtilityTest : TestCase() { expectedBundle.putLong(Const.ASSIGNMENT_ID, 123456789) val parentFragment = callGetFragment(moduleItem, course, null) - TestCase.assertNotNull(parentFragment) - TestCase.assertEquals(AssignmentDetailsFragment::class.java, parentFragment!!.javaClass) - TestCase.assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) + assertNotNull(parentFragment) + assertEquals(AssignmentDetailsFragment::class.java, parentFragment!!.javaClass) + assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) } @Test @@ -163,9 +164,9 @@ class ModuleUtilityTest : TestCase() { expectedBundle.putLong(Const.ASSIGNMENT_ID, 123456789) val parentFragment = callGetFragment(moduleItem, course, null, isOnline = false, tabs = setOf(Tab.ASSIGNMENTS_ID)) - TestCase.assertNotNull(parentFragment) - TestCase.assertEquals(AssignmentDetailsFragment::class.java, parentFragment!!.javaClass) - TestCase.assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) + assertNotNull(parentFragment) + assertEquals(AssignmentDetailsFragment::class.java, parentFragment!!.javaClass) + assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) } @Test @@ -180,8 +181,8 @@ class ModuleUtilityTest : TestCase() { val course = Course() val fragment = callGetFragment(moduleItem, course, null, isOnline = false) - TestCase.assertNotNull(fragment) - TestCase.assertEquals(NotAvailableOfflineFragment::class.java, fragment!!.javaClass) + assertNotNull(fragment) + assertEquals(NotAvailableOfflineFragment::class.java, fragment!!.javaClass) } @Test @@ -199,9 +200,9 @@ class ModuleUtilityTest : TestCase() { expectedBundle.putLong(Const.ASSIGNMENT_ID, 123450000000006789) val parentFragment = callGetFragment(moduleItem, course, null) - TestCase.assertNotNull(parentFragment) - TestCase.assertEquals(AssignmentDetailsFragment::class.java, parentFragment!!.javaClass) - TestCase.assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) + assertNotNull(parentFragment) + assertEquals(AssignmentDetailsFragment::class.java, parentFragment!!.javaClass) + assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) } @Test @@ -219,9 +220,9 @@ class ModuleUtilityTest : TestCase() { expectedBundle.putLong(Const.ASSIGNMENT_ID, 123450000000006789) val parentFragment = callGetFragment(moduleItem, course, null) - TestCase.assertNotNull(parentFragment) - TestCase.assertEquals(AssignmentDetailsFragment::class.java, parentFragment!!.javaClass) - TestCase.assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) + assertNotNull(parentFragment) + assertEquals(AssignmentDetailsFragment::class.java, parentFragment!!.javaClass) + assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) } @Test @@ -245,16 +246,16 @@ class ModuleUtilityTest : TestCase() { expectedBundle.putBoolean(com.instructure.pandautils.utils.Const.IS_UNSUPPORTED_FEATURE, true) var parentFragment = callGetFragment(moduleItem, course, null) - TestCase.assertNotNull(parentFragment) - TestCase.assertEquals(InternalWebviewFragment::class.java, parentFragment!!.javaClass) - TestCase.assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) + assertNotNull(parentFragment) + assertEquals(InternalWebviewFragment::class.java, parentFragment!!.javaClass) + assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) // test external tool type val moduleItem2 = moduleItem.copy(type = "ExternalTool") parentFragment = callGetFragment(moduleItem2, course, null) - TestCase.assertNotNull(parentFragment) - TestCase.assertEquals(InternalWebviewFragment::class.java, parentFragment!!.javaClass) - TestCase.assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) + assertNotNull(parentFragment) + assertEquals(InternalWebviewFragment::class.java, parentFragment!!.javaClass) + assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) } @Test @@ -271,8 +272,8 @@ class ModuleUtilityTest : TestCase() { val course = Course() val fragment = callGetFragment(moduleItem, course, null, isOnline = false) - TestCase.assertNotNull(fragment) - TestCase.assertEquals(NotAvailableOfflineFragment::class.java, fragment!!.javaClass) + assertNotNull(fragment) + assertEquals(NotAvailableOfflineFragment::class.java, fragment!!.javaClass) } @Test @@ -308,9 +309,9 @@ class ModuleUtilityTest : TestCase() { expectedBundle.putLong(Const.ID, 55) val parentFragment = callGetFragment(moduleItem, course, null) - TestCase.assertNotNull(parentFragment) - TestCase.assertEquals(ModuleQuizDecider::class.java, parentFragment!!.javaClass) - TestCase.assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) + assertNotNull(parentFragment) + assertEquals(ModuleQuizDecider::class.java, parentFragment!!.javaClass) + assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) } @Test @@ -326,12 +327,10 @@ class ModuleUtilityTest : TestCase() { val expectedBundle = Bundle() expectedBundle.putParcelable(Const.CANVAS_CONTEXT, course) expectedBundle.putLong(DiscussionDetailsFragment.DISCUSSION_TOPIC_HEADER_ID, 123456789) - expectedBundle.putString(DiscussionDetailsFragment.DISCUSSION_TITLE, null) - expectedBundle.putBoolean(DiscussionDetailsFragment.GROUP_DISCUSSION, false) val parentFragment = callGetFragment(moduleItem, course, null) - TestCase.assertNotNull(parentFragment) - TestCase.assertEquals(DiscussionDetailsFragment::class.java, parentFragment!!.javaClass) - TestCase.assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) + assertNotNull(parentFragment) + assertEquals(DiscussionDetailsWebViewFragment::class.java, parentFragment!!.javaClass) + assertEquals(expectedBundle.toString(), parentFragment.arguments!!.toString()) } @Test @@ -345,11 +344,11 @@ class ModuleUtilityTest : TestCase() { val course = Course() val fragment = callGetFragment(moduleItem, course, null, isOnline = false) - TestCase.assertNotNull(fragment) - TestCase.assertEquals(NotAvailableOfflineFragment::class.java, fragment!!.javaClass) + assertNotNull(fragment) + assertEquals(NotAvailableOfflineFragment::class.java, fragment!!.javaClass) } private fun callGetFragment(moduleItem: ModuleItem, course: Course, moduleObject: ModuleObject?, isOnline: Boolean = true, tabs: Set = emptySet(), files: List = emptyList()): Fragment? { - return ModuleUtility.getFragment(moduleItem, course, moduleObject, false, false, isOnline, tabs, files, context) + return ModuleUtility.getFragment(moduleItem, course, moduleObject, false, isOnline, tabs, files, context) } } diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCalendarPageTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCalendarPageTest.kt index 98a96f99f8..649e9c65b8 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCalendarPageTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/TeacherCalendarPageTest.kt @@ -21,10 +21,11 @@ import com.instructure.canvas.espresso.mockCanvas.init import com.instructure.canvasapi2.models.User import com.instructure.espresso.ModuleItemInteractions import com.instructure.teacher.BuildConfig +import com.instructure.teacher.R import com.instructure.teacher.activities.LoginActivity import com.instructure.teacher.ui.pages.AssignmentDetailsPage import com.instructure.teacher.ui.pages.DashboardPage -import com.instructure.teacher.ui.pages.NativeDiscussionsDetailsPage +import com.instructure.teacher.ui.pages.DiscussionsDetailsPage import com.instructure.teacher.ui.utils.TeacherActivityTestRule import com.instructure.teacher.ui.utils.tokenLogin import dagger.hilt.android.testing.HiltAndroidTest @@ -38,7 +39,7 @@ class TeacherCalendarPageTest : CalendarInteractionTest() { private val dashboardPage = DashboardPage() private val assignmentDetailsPage = AssignmentDetailsPage(ModuleItemInteractions()) - private val discussionDetailsPage = NativeDiscussionsDetailsPage(ModuleItemInteractions()) + private val discussionDetailsPage = DiscussionsDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next, R.id.previous)) override fun goToCalendar(data: MockCanvas) { val teacher = data.teachers[0] @@ -67,6 +68,6 @@ class TeacherCalendarPageTest : CalendarInteractionTest() { } override fun assertDiscussionDetailsTitle(title: String) { - discussionDetailsPage.assertDiscussionTitle(title) + discussionDetailsPage.assertToolbarDiscussionTitle(title) } } \ No newline at end of file diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/NativeDiscussionsDetailsPage.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/NativeDiscussionsDetailsPage.kt deleted file mode 100644 index 9f64eac28a..0000000000 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/pages/NativeDiscussionsDetailsPage.kt +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (C) 2021 - present Instructure, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ -package com.instructure.teacher.ui.pages - -import com.instructure.espresso.ModuleItemInteractions -import com.instructure.espresso.assertDisplayed -import com.instructure.espresso.assertHasText -import com.instructure.espresso.assertNotDisplayed -import com.instructure.espresso.click -import com.instructure.espresso.page.BasePage -import com.instructure.espresso.page.onView -import com.instructure.espresso.page.withId -import com.instructure.espresso.scrollTo -import com.instructure.espresso.swipeDown -import com.instructure.teacher.R -import com.instructure.teacher.ui.utils.TypeInRCETextEditor - -class NativeDiscussionsDetailsPage(val moduleItemInteractions: ModuleItemInteractions) : BasePage() { - - /** - * Asserts that the discussion has the specified [title]. - * - * @param title The title of the discussion to be asserted. - */ - fun assertDiscussionTitle(title: String) { - onView(withId(R.id.discussionTopicTitle)).assertHasText(title) - } - - /** - * Asserts that the discussion is published. - */ - fun assertDiscussionPublished() { - checkPublishedTextView("Published") - } - - /** - * Asserts that the discussion is unpublished. - */ - fun assertDiscussionUnpublished() { - checkPublishedTextView("Unpublished") - } - - /** - * Asserts that there are no replies in the discussion. - */ - fun assertNoReplies() { - onView(withId(R.id.discussionTopicReplies)).assertNotDisplayed() - } - - /** - * Asserts that the discussion has at least one reply. - */ - fun assertHasReply() { - val repliesHeader = onView(withId(R.id.discussionTopicReplies)) - repliesHeader.scrollTo() - repliesHeader.assertDisplayed() - } - - /** - * Opens the edit menu of the discussion. - */ - fun openEdit() { - onView(withId(R.id.menu_edit)).click() - } - - /** - * Refreshes the discussion page. - */ - fun refresh() { - onView(withId(R.id.swipeRefreshLayout)).swipeDown() - } - - /** - * Adds a reply with the specified [content] to the discussion. - * - * @param content The content of the reply. - */ - fun addReply(content: String) { - onView(withId(R.id.replyToDiscussionTopic)).click() - onView(withId(R.id.rce_webView)).perform(TypeInRCETextEditor(content)) - onView(withId(R.id.menu_send)).click() - } - - private fun checkPublishedTextView(status: String) { - onView(withId(R.id.publishStatusTextView)).assertHasText(status) - } -} \ No newline at end of file diff --git a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt index 2f0c4e3d04..de486454e7 100644 --- a/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt +++ b/apps/teacher/src/androidTest/java/com/instructure/teacher/ui/utils/TeacherTest.kt @@ -63,7 +63,6 @@ import com.instructure.teacher.ui.pages.LoginFindSchoolPage import com.instructure.teacher.ui.pages.LoginLandingPage import com.instructure.teacher.ui.pages.LoginSignInPage import com.instructure.teacher.ui.pages.ModulesPage -import com.instructure.teacher.ui.pages.NativeDiscussionsDetailsPage import com.instructure.teacher.ui.pages.NavDrawerPage import com.instructure.teacher.ui.pages.NotATeacherPage import com.instructure.teacher.ui.pages.PageListPage @@ -123,7 +122,6 @@ abstract class TeacherTest : CanvasTest() { val remoteConfigSettingsPage = RemoteConfigSettingsPage() val profileSettingsPage = ProfileSettingsPage() val editProfileSettingsPage = EditProfileSettingsPage() - val nativeDiscussionsDetailsPage = NativeDiscussionsDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next, R.id.previous)) val discussionDetailsPage = DiscussionsDetailsPage(ModuleItemInteractions(R.id.moduleName, R.id.next, R.id.previous)) val discussionsListPage = DiscussionsListPage(Searchable(R.id.search, R.id.search_src_text, R.id.search_close_btn)) val editAnnouncementDetailsPage = EditAnnouncementDetailsPage() diff --git a/apps/teacher/src/main/java/com/instructure/teacher/events/BusEvents.kt b/apps/teacher/src/main/java/com/instructure/teacher/events/BusEvents.kt index 91dfbe80fe..e1a5f8ced8 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/events/BusEvents.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/events/BusEvents.kt @@ -164,15 +164,9 @@ class DiscussionUpdatedEvent(discussionTopicHeader: DiscussionTopicHeader, skipI /** Convenience function for posting this event to the EventBus */ fun RationedBusEvent<*>.post() = EventBus.getDefault().postSticky(this) -/** A RationedBusEvent for DiscussionEntry changes. @see [RationedBusEvent] */ -class DiscussionEntryEvent(discussionEntry: DiscussionEntry, skipId: String? = null) : RationedBusEvent(discussionEntry, skipId) - /** A RationedBusEvent for DiscussionEntry updates. @see [RationedBusEvent] */ class DiscussionEntryUpdatedEvent(discussionEntry: DiscussionEntry, skipId: String? = null) : RationedBusEvent(discussionEntry, skipId) -/** A RationedBusEvent for DiscussionTopic changes. @see [RationedBusEvent] */ -class DiscussionTopicEvent(discussionTopic: DiscussionTopic, skipId: String? = null) : RationedBusEvent(discussionTopic, skipId) - /** A RationedBusEvent for DiscussionTopicHeader changes. @see [RationedBusEvent] */ class DiscussionTopicHeaderEvent(discussionTopicHeader: DiscussionTopicHeader, skipId: String? = null) : RationedBusEvent(discussionTopicHeader, skipId) diff --git a/apps/teacher/src/main/java/com/instructure/teacher/factory/DiscussionsDetailsPresenterFactory.kt b/apps/teacher/src/main/java/com/instructure/teacher/factory/DiscussionsDetailsPresenterFactory.kt deleted file mode 100644 index cde7e833e1..0000000000 --- a/apps/teacher/src/main/java/com/instructure/teacher/factory/DiscussionsDetailsPresenterFactory.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2017 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.instructure.teacher.factory - -import com.instructure.canvasapi2.models.CanvasContext -import com.instructure.canvasapi2.models.DiscussionTopic -import com.instructure.canvasapi2.models.DiscussionTopicHeader -import com.instructure.teacher.presenters.DiscussionsDetailsPresenter -import com.instructure.teacher.viewinterface.DiscussionsDetailsView -import instructure.androidblueprint.PresenterFactory - -class DiscussionsDetailsPresenterFactory( - val canvasContext: CanvasContext, - val discussionTopicHeader: DiscussionTopicHeader, - val discussionTopic: DiscussionTopic, - val skipId: String) : PresenterFactory { - override fun create() = DiscussionsDetailsPresenter( - canvasContext, discussionTopicHeader, discussionTopic, skipId) -} diff --git a/apps/teacher/src/main/java/com/instructure/teacher/factory/DiscussionsReplyFactory.kt b/apps/teacher/src/main/java/com/instructure/teacher/factory/DiscussionsReplyFactory.kt deleted file mode 100644 index 91ec1c8286..0000000000 --- a/apps/teacher/src/main/java/com/instructure/teacher/factory/DiscussionsReplyFactory.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2017 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ -package com.instructure.teacher.factory - -import com.instructure.canvasapi2.models.CanvasContext -import com.instructure.teacher.presenters.DiscussionsReplyPresenter -import com.instructure.teacher.viewinterface.DiscussionsReplyView -import instructure.androidblueprint.PresenterFactory - -class DiscussionsReplyFactory( - val canvasContext: CanvasContext, - val discussionTopicHeaderId: Long, - val discussionEntryId: Long) : PresenterFactory { - - override fun create(): DiscussionsReplyPresenter { - return DiscussionsReplyPresenter(canvasContext, discussionTopicHeaderId, discussionEntryId) - } -} diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/DiscussionsDetailsFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/DiscussionsDetailsFragment.kt deleted file mode 100644 index 017718db54..0000000000 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/DiscussionsDetailsFragment.kt +++ /dev/null @@ -1,843 +0,0 @@ -/* - * Copyright (C) 2017 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.instructure.teacher.features.discussion - -import android.annotation.SuppressLint -import android.graphics.Rect -import android.os.Bundle -import android.view.LayoutInflater -import android.view.MenuItem -import android.view.View -import android.webkit.CookieManager -import android.webkit.JavascriptInterface -import android.webkit.WebView -import android.widget.ScrollView -import androidx.appcompat.app.AlertDialog -import androidx.core.content.ContextCompat -import com.instructure.canvasapi2.models.* -import com.instructure.canvasapi2.utils.* -import com.instructure.canvasapi2.utils.pageview.PageView -import com.instructure.canvasapi2.utils.pageview.PageViewUrlParam -import com.instructure.canvasapi2.utils.weave.WeaveJob -import com.instructure.canvasapi2.utils.weave.catch -import com.instructure.canvasapi2.utils.weave.tryWeave -import com.instructure.interactions.FullScreenInteractions -import com.instructure.interactions.Identity -import com.instructure.interactions.MasterDetailInteractions -import com.instructure.interactions.router.Route -import com.instructure.pandautils.analytics.SCREEN_VIEW_DISCUSSION_DETAILS -import com.instructure.pandautils.analytics.ScreenView -import com.instructure.pandautils.dialogs.AttachmentPickerDialog -import com.instructure.pandautils.discussions.DiscussionCaching -import com.instructure.pandautils.discussions.DiscussionEntryHtmlConverter -import com.instructure.pandautils.discussions.DiscussionUtils -import com.instructure.pandautils.fragments.BasePresenterFragment -import com.instructure.pandautils.utils.* -import com.instructure.pandautils.utils.Const -import com.instructure.pandautils.views.CanvasWebView -import com.instructure.teacher.BuildConfig -import com.instructure.teacher.R -import com.instructure.teacher.activities.InternalWebViewActivity -import com.instructure.teacher.adapters.StudentContextFragment -import com.instructure.teacher.databinding.FragmentDiscussionsDetailsBinding -import com.instructure.teacher.dialog.NoInternetConnectionDialog -import com.instructure.teacher.events.* -import com.instructure.teacher.events.DiscussionEntryEvent -import com.instructure.teacher.factory.DiscussionsDetailsPresenterFactory -import com.instructure.teacher.features.assignment.submission.AssignmentSubmissionListFragment -import com.instructure.teacher.fragments.* -import com.instructure.teacher.features.assignment.submission.AssignmentSubmissionListPresenter -import com.instructure.teacher.features.assignment.submission.SubmissionListFilter -import com.instructure.teacher.presenters.DiscussionsDetailsPresenter -import com.instructure.teacher.router.RouteMatcher -import com.instructure.teacher.utils.* -import com.instructure.teacher.viewinterface.DiscussionsDetailsView -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode -import java.util.* - -@PageView(url = "{canvasContext}/{type}/{topicId}") -@ScreenView(SCREEN_VIEW_DISCUSSION_DETAILS) -class DiscussionsDetailsFragment : BasePresenterFragment< - DiscussionsDetailsPresenter, - DiscussionsDetailsView, - FragmentDiscussionsDetailsBinding>(), DiscussionsDetailsView, Identity { - - //region Member Variables - private var canvasContext: CanvasContext by ParcelableArg(key = Const.CANVAS_CONTEXT) - private var discussionTopicHeader: DiscussionTopicHeader by ParcelableArg(DiscussionTopicHeader(), DISCUSSION_TOPIC_HEADER) - private var discussionTopic: DiscussionTopic by ParcelableArg(DiscussionTopic(), DISCUSSION_TOPIC) - private var discussionEntryId: Long by LongArg(0L, DISCUSSION_ENTRY_ID) - private var discussionTopicHeaderId: Long by LongArg(0L, DISCUSSION_TOPIC_HEADER_ID) - private var skipIdentityCheck: Boolean by BooleanArg(false, SKIP_IDENTITY_CHECK) - private var skipId: String by StringArg("", SKIP_ID) - - private var isAnnouncements: Boolean by BooleanArg(false, IS_ANNOUNCEMENT) - private var isNestedDetail: Boolean by BooleanArg(false, IS_NESTED_DETAIL) - - private var repliesLoadHtmlJob: Job? = null - private var headerLoadHtmlJob: Job? = null - private var loadDiscussionJob: WeaveJob? = null - - //endregion - - @Suppress("unused") - @PageViewUrlParam("topicId") - private fun getTopicId() = discussionTopicHeader.id - - override val bindingInflater: (layoutInflater: LayoutInflater) -> FragmentDiscussionsDetailsBinding = FragmentDiscussionsDetailsBinding::inflate - - override fun onRefreshFinished() { - binding.discussionProgressBar.setGone() - } - - override fun onRefreshStarted() { - binding.discussionProgressBar.setVisible() - } - - override fun onStart() { - super.onStart() - EventBus.getDefault().register(this) - } - - override fun onStop() { - super.onStop() - EventBus.getDefault().unregister(this) - } - - override fun onDestroyView() { - super.onDestroyView() - loadDiscussionJob?.cancel() - repliesLoadHtmlJob?.cancel() - headerLoadHtmlJob?.cancel() - } - - override val identity: Long? get() = if(discussionTopicHeaderId != 0L) discussionTopicHeaderId else discussionTopicHeader.id - override val skipCheck: Boolean get() = skipIdentityCheck - - override fun getPresenterFactory() = - DiscussionsDetailsPresenterFactory(canvasContext, discussionTopicHeader, discussionTopic, - if(skipId.isEmpty()) DiscussionsDetailsFragment::class.java.simpleName + UUID.randomUUID().toString() else skipId) - - override fun onPresenterPrepared(presenter: DiscussionsDetailsPresenter) {} - - override fun onReadySetGo(presenter: DiscussionsDetailsPresenter) { - - EventBus.getDefault().getStickyEvent(DiscussionEntryUpdatedEvent::class.java)?.once(javaClass.simpleName) { discussionEntry -> - presenter.updateDiscussionEntryToDiscussionTopic(discussionEntry) - } - - val discussionTopicEvent = EventBus.getDefault().getStickyEvent(DiscussionTopicEvent::class.java) - - if(discussionTopicEvent != null) { - discussionTopicEvent.only(presenter.getSkipId()) { discussionTopic -> - //A The Discussion Topic was changed in some way. Usually from a nested situation where something was added. - presenter.updateDiscussionTopic(discussionTopic) - if (!isNestedDetail) { - EventBus.getDefault().removeStickyEvent(discussionTopicEvent) - } - } - } else { - if (discussionTopicHeaderId == 0L && presenter.discussionTopicHeader.id != 0L) { - //We were given a valid DiscussionTopicHeader, no need to fetch from the API - populateDiscussionTopicHeader(presenter.discussionTopicHeader, false) - } else if (discussionTopicHeaderId != 0L) { - //results of this GET will call populateDiscussionTopicHeader() - presenter.getDiscussionTopicHeader(discussionTopicHeaderId) - } - } - - EventBus.getDefault().getStickyEvent(DiscussionTopicHeaderDeletedEvent::class.java)?.once(javaClass.simpleName + ".onResume()") { - if (it == presenter.discussionTopicHeader.id) { - if (activity is MasterDetailInteractions) { - (activity as MasterDetailInteractions).popFragment(canvasContext) - } else if(activity is FullScreenInteractions) { - requireActivity().finish() - } - } - } - - EventBus.getDefault().getStickyEvent(DiscussionEntryEvent::class.java)?.once(javaClass.simpleName) { discussionEntry -> - presenter.addDiscussionEntryToDiscussionTopic(discussionEntry) - } - } - - override fun populateAsForbidden() { - //TODO: when we add support for students - } - - override fun populateDiscussionTopicHeader(discussionTopicHeader: DiscussionTopicHeader, forceNetwork: Boolean) = with(binding) { - if(discussionTopicHeader.assignment != null) { - setupAssignmentDetails(discussionTopicHeader.assignment!!) - presenter.getSubmissionData(forceNetwork) - setupListeners() - } - - // Publish status if discussion - if(!isAnnouncements) { - if (discussionTopicHeader.published) { - publishStatusIconView.setImageResource(R.drawable.ic_complete_solid) - publishStatusIconView.setColorFilter(requireContext().getColorCompat(R.color.textSuccess)) - publishStatusTextView.setText(R.string.published) - publishStatusTextView.setTextColor(requireContext().getColorCompat(R.color.textSuccess)) - } else { - publishStatusIconView.setImageResource(R.drawable.ic_complete) - publishStatusIconView.setColorFilter(requireContext().getColorCompat(R.color.textDark)) - publishStatusTextView.setText(R.string.not_published) - publishStatusTextView.setTextColor(requireContext().getColorCompat(R.color.textDark)) - } - } else { - pointsPublishedLayout.setGone() - pointsPublishedDivider.root.setGone() - dueLayoutDivider.root.setGone() - submissionDivider.root.setGone() - } - - // If we're getting here by a deep link (like from an email) we don't know that it is an announcement - // because that field is not included in the api (:frowny_face:) The sections field won't show up for a discussion or - // an announcement that isn't section specific - discussionTopicHeader.sections?.joinToString { it.name }?.validOrNull()?.let { - announcementSection.setVisible().text = it - } - - loadDiscussionTopicHeader(discussionTopicHeader) - repliesBack.setVisible(isNestedDetail) - repliesBack.onClick { requireActivity().onBackPressed() } - attachmentIcon.setVisible(!discussionTopicHeader.attachments.isEmpty()) - attachmentIcon.onClick { - val remoteFiles = presenter.discussionTopicHeader.attachments - if(remoteFiles != null) { - viewAttachments(remoteFiles) - } - } - - if(presenter.discussionTopic.views.isEmpty()) { - // Loading data will eventually call, upon success, populateDiscussionTopic() - presenter.loadData(true) - } else { - populateDiscussionTopic(discussionTopicHeader, presenter.discussionTopic) - } - } - - override fun populateDiscussionTopic(discussionTopicHeader: DiscussionTopicHeader, discussionTopic: DiscussionTopic, topLevelReplyPosted: Boolean) = with(binding) { - // Check if we have permissions and if we have any discussions to display. - - loadDiscussionJob = tryWeave { - - swipeRefreshLayout.isRefreshing = false - - if(discussionTopic.views.isEmpty() && DiscussionCaching(discussionTopicHeader.id).isEmpty()) { - // Nothing to display - discussionRepliesHeaderWrapper.setGone() - return@tryWeave - } - - discussionRepliesHeaderWrapper.setVisible() - - val html = inBackground { - DiscussionUtils.createDiscussionTopicHtml( - requireActivity(), - isTablet, - canvasContext, - discussionTopicHeader, - discussionTopic.views, - discussionEntryId) - } - - discussionRepliesWebViewWrapper.setInvisible() - - repliesLoadHtmlJob = discussionRepliesWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), html, {formattedHtml -> - discussionRepliesWebViewWrapper.loadDataWithBaseUrl(CanvasWebView.getReferrer(true), formattedHtml, "text/html", "utf-8", null) - }) { - LtiLaunchFragment.routeLtiLaunchFragment(requireActivity(), canvasContext, it) - } - - delay(300) - discussionsScrollView.post { - if (topLevelReplyPosted) { - discussionsScrollView.fullScroll(ScrollView.FOCUS_DOWN) - } else { - discussionsScrollView.scrollTo(0, presenter.scrollPosition) - } - discussionRepliesWebViewWrapper.setVisible() - } - } catch { Logger.e("Error loading discussion " + it.message) } - } - - private fun setupAssignmentDetails(assignment: Assignment) = with(binding) { - pointsTextView.setVisible() - // Points possible - pointsTextView.text = resources.getQuantityString( - R.plurals.quantityPointsAbbreviated, - assignment.pointsPossible.toInt(), - NumberHelper.formatDecimal(assignment.pointsPossible, 1, true) - ) - pointsTextView.contentDescription = resources.getQuantityString( - R.plurals.quantityPointsFull, - assignment.pointsPossible.toInt(), - NumberHelper.formatDecimal(assignment.pointsPossible, 1, true)) - - dueLayout.setVisible() - submissionsLayout.setVisible((canvasContext as? Course)?.isDesigner() == false) - - //set these as gone and make them visible if we have data for them - availabilityLayout.setGone() - availableFromLayout.setGone() - availableToLayout.setGone() - dueForLayout.setGone() - dueDateLayout.setGone() - otherDueDateTextView.setGone() - - // Lock status - val atSeparator = getString(R.string.at) - - val allDates = assignment.allDates - allDates.singleOrNull()?.apply { - if (assignment.lockDate?.before(Date()) == true) { - availabilityLayout.setVisible() - availabilityTextView.setText(R.string.closed) - } else { - availableFromLayout.setVisible() - availableToLayout.setVisible() - availableFromTextView.text = if (unlockAt != null) - DateHelper.getMonthDayAtTime(requireContext(), unlockDate, atSeparator) else getString(R.string.no_date_filler) - availableToTextView.text = if (lockAt != null) - DateHelper.getMonthDayAtTime(requireContext(), lockDate, atSeparator) else getString(R.string.no_date_filler) - } - } - - // Due date(s) - if (allDates.size > 1) { - otherDueDateTextView.setVisible() - otherDueDateTextView.setText(R.string.multiple_due_dates) - } else { - if (allDates.isEmpty() || allDates[0].dueAt == null) { - otherDueDateTextView.setVisible() - otherDueDateTextView.setText(R.string.no_due_date) - - dueForLayout.setVisible() - dueForTextView.text = if (allDates.isEmpty() || allDates[0].isBase) getString(R.string.everyone) else allDates[0].title ?: "" - - } else with(allDates[0]) { - dueDateLayout.setVisible() - dueDateTextView.text = DateHelper.getMonthDayAtTime(requireContext(), dueDate, atSeparator) - - dueForLayout.setVisible() - dueForTextView.text = if (isBase) getString(R.string.everyone) else title ?: "" - } - } - - } - - override fun updateSubmissionDonuts(totalStudents: Int, gradedStudents: Int, needsGradingCount: Int, notSubmitted: Int) = with(binding.donutGroup) { - // Submission section - gradedChart.setSelected(gradedStudents) - gradedChart.setTotal(totalStudents) - gradedChart.setSelectedColor(ThemePrefs.brandColor) - gradedChart.setCenterText(gradedStudents.toString()) - gradedWrapper.contentDescription = getString(R.string.content_description_submission_donut_graded).format(gradedStudents, totalStudents) - gradedProgressBar.setGone() - gradedChart.invalidate() - - ungradedChart.setSelected(needsGradingCount) - ungradedChart.setTotal(totalStudents) - ungradedChart.setSelectedColor(ThemePrefs.brandColor) - ungradedChart.setCenterText(needsGradingCount.toString()) - ungradedLabel.text = requireContext().resources.getQuantityText(R.plurals.needsGradingNoQuantity, needsGradingCount) - ungradedWrapper.contentDescription = getString(R.string.content_description_submission_donut_needs_grading).format(needsGradingCount, totalStudents) - ungradedProgressBar.setGone() - ungradedChart.invalidate() - - notSubmittedChart.setSelected(notSubmitted) - notSubmittedChart.setTotal(totalStudents) - notSubmittedChart.setSelectedColor(ThemePrefs.brandColor) - notSubmittedChart.setCenterText(notSubmitted.toString()) - notSubmittedWrapper.contentDescription = getString(R.string.content_description_submission_donut_unsubmitted).format(notSubmitted, totalStudents) - notSubmittedProgressBar.setGone() - notSubmittedChart.invalidate() - } - - private fun setupListeners() = with(binding) { - dueLayout.setOnClickListener { - val args = DueDatesFragment.makeBundle(presenter.discussionTopicHeader.assignment!!) - RouteMatcher.route(requireActivity(), Route(null, DueDatesFragment::class.java, canvasContext, args)) - } - submissionsLayout.setOnClickListener { - navigateToSubmissions(canvasContext, presenter.discussionTopicHeader.assignment!!, SubmissionListFilter.ALL) - } - binding.donutGroup.viewAllSubmissions.onClick { submissionsLayout.performClick() } // Separate click listener for a11y - binding.donutGroup.gradedWrapper.setOnClickListener { - navigateToSubmissions(canvasContext, presenter.discussionTopicHeader.assignment!!, SubmissionListFilter.GRADED) - } - binding.donutGroup.ungradedWrapper.setOnClickListener { - navigateToSubmissions(canvasContext, presenter.discussionTopicHeader.assignment!!, SubmissionListFilter.NOT_GRADED) - } - binding.donutGroup.notSubmittedWrapper.setOnClickListener { - navigateToSubmissions(canvasContext, presenter.discussionTopicHeader.assignment!!, SubmissionListFilter.MISSING) - } - } - - private fun navigateToSubmissions(context: CanvasContext, assignment: Assignment, filter: SubmissionListFilter) { - val args = AssignmentSubmissionListFragment.makeBundle(assignment, filter) - RouteMatcher.route(requireActivity(), Route(null, AssignmentSubmissionListFragment::class.java, context, args)) - } - - private fun loadDiscussionTopicHeader(discussionTopicHeader: DiscussionTopicHeader) = with(binding) { - val displayName = discussionTopicHeader.author?.displayName - ProfileUtils.loadAvatarForUser(authorAvatar, displayName, discussionTopicHeader.author?.avatarImageUrl) - authorAvatar.setupAvatarA11y(discussionTopicHeader.author?.displayName) - authorAvatar.onClick { - val bundle = StudentContextFragment.makeBundle(discussionTopicHeader.author?.id ?: 0, canvasContext.id) - RouteMatcher.route(requireActivity(), Route(StudentContextFragment::class.java, null, bundle)) - } - authorName?.text = discussionTopicHeader.author?.let { Pronouns.span(it.displayName, it.pronouns) } - authoredDate?.text = DateHelper.getMonthDayAtTime(requireContext(), discussionTopicHeader.postedDate, getString(R.string.at)) - discussionTopicTitle?.text = discussionTopicHeader.title - - replyToDiscussionTopic.setTextColor(ThemePrefs.textButtonColor) - replyToDiscussionTopic.setVisible(discussionTopicHeader.permissions!!.reply) - replyToDiscussionTopic.onClick { - showReplyView(presenter.discussionTopicHeader.id) - } - - headerLoadHtmlJob = discussionTopicHeaderWebViewWrapper.webView.loadHtmlWithIframes(requireContext(), discussionTopicHeader.message, { - discussionTopicHeaderWebViewWrapper.loadHtml(it, discussionTopicHeader.title, baseUrl = this@DiscussionsDetailsFragment.discussionTopicHeader.htmlUrl) - }) { - LtiLaunchFragment.routeLtiLaunchFragment(requireActivity(), canvasContext, it) - } - - discussionRepliesWebViewWrapper.loadHtml("", "") - } - - override fun onPause() = with(binding) { - super.onPause() - presenter.scrollPosition = discussionsScrollView.scrollY - discussionTopicHeaderWebViewWrapper.webView.onPause() - discussionRepliesWebViewWrapper.webView.onPause() - } - - override fun onResume() = with(binding) { - super.onResume() - setupToolbar() - - if (isAccessibilityEnabled(requireContext()) && discussionTopicHeader.htmlUrl != null) { - alternateViewButton.visibility = View.VISIBLE - alternateViewButton.setOnClickListener { - val bundle = InternalWebViewFragment.makeBundle( - discussionTopicHeader.htmlUrl!!, - discussionTopicHeader.title!!, - shouldAuthenticate = true - ) - RouteMatcher.route(requireActivity(), Route(null, InternalWebViewFragment::class.java, canvasContext, bundle)) - } - } - - swipeRefreshLayout.setOnRefreshListener { - presenter.loadData(true) - presenter.getSubmissionData(true) - - // Send out bus events to trigger a refresh for discussion list and submission list - DiscussionUpdatedEvent(presenter.discussionTopicHeader, javaClass.simpleName).post() - presenter.discussionTopicHeader.assignment?.let { - AssignmentGradedEvent(it.id, javaClass.simpleName).post() - } - } - - discussionTopicHeaderWebViewWrapper.webView.onResume() - discussionRepliesWebViewWrapper.webView.onResume() - - setupWebView(discussionTopicHeaderWebViewWrapper.webView, false) - setupWebView(discussionRepliesWebViewWrapper.webView, true, addDarkTheme = !discussionRepliesWebViewWrapper.themeSwitched) - discussionRepliesWebViewWrapper.onThemeChanged = { themeChanged, html -> - setupWebView(discussionRepliesWebViewWrapper.webView, true, addDarkTheme = !themeChanged) - discussionRepliesWebViewWrapper.loadDataWithBaseUrl(CanvasWebView.getReferrer(true), html, "text/html", "UTF-8", null) - } - } - - private fun setupToolbar() = with(binding) { - toolbar.setupBackButtonWithExpandCollapseAndBack(this@DiscussionsDetailsFragment) { - toolbar.updateToolbarExpandCollapseIcon(this@DiscussionsDetailsFragment) - ViewStyler.themeToolbarColored(requireActivity(), toolbar, canvasContext.backgroundColor, requireContext().getColor(R.color.white)) - (activity as MasterDetailInteractions).toggleExpandCollapse() - } - toolbar.setupMenu(R.menu.menu_edit_generic, menuItemCallback) - toolbar.title = if(isAnnouncements) getString(R.string.announcementDetails) else getString(R.string.discussion_details) - if(!isTablet) { - toolbar.subtitle = canvasContext.name - } - ViewStyler.themeToolbarColored(requireActivity(), toolbar, canvasContext.backgroundColor, requireContext().getColor(R.color.white)) - } - - private val menuItemCallback: (MenuItem) -> Unit = { item -> - when (item.itemId) { - R.id.menu_edit -> { - if(APIHelper.hasNetworkConnection()) { - if(isAnnouncements) { - val args = CreateOrEditAnnouncementFragment.newInstanceEdit(presenter.canvasContext, presenter.discussionTopicHeader).nonNullArgs - RouteMatcher.route(requireActivity(), Route(CreateOrEditAnnouncementFragment::class.java, null, args)) - } else { - // If we have an assignment, set the topic header to null to prevent cyclic reference - presenter.discussionTopicHeader.assignment?.discussionTopicHeader = null - val args = CreateDiscussionFragment.makeBundle(presenter.canvasContext, presenter.discussionTopicHeader) - RouteMatcher.route(requireActivity(), Route(CreateDiscussionFragment::class.java, canvasContext, args)) - } - } else { - NoInternetConnectionDialog.show(requireFragmentManager()) - } - } - } - } - - @SuppressLint("SetJavaScriptEnabled") - private fun setupWebView(webView: CanvasWebView, addJSSupport: Boolean, addDarkTheme: Boolean = false) { - WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG) - val backgroundColorRes = if (addDarkTheme) R.color.backgroundLightest else R.color.white - webView.setBackgroundColor(requireContext().getColor(backgroundColorRes)) - webView.settings.javaScriptEnabled = true - if(addJSSupport) webView.addJavascriptInterface(JSDiscussionInterface(), "accessor") - webView.settings.useWideViewPort = true - webView.settings.loadWithOverviewMode = true - CookieManager.getInstance().acceptThirdPartyCookies(webView) - webView.canvasWebViewClientCallback = object: CanvasWebView.CanvasWebViewClientCallback { - override fun routeInternallyCallback(url: String) { - if (!RouteMatcher.canRouteInternally(activity, url, ApiPrefs.domain, true)) { - val bundle = InternalWebViewFragment.makeBundle(url, url, false, "") - RouteMatcher.route(requireActivity(), Route(FullscreenInternalWebViewFragment::class.java, - presenter.canvasContext, bundle)) - } - } - override fun canRouteInternallyDelegate(url: String): Boolean { - return true - } - override fun openMediaFromWebView(mime: String, url: String, filename: String) { - showToast(R.string.downloadingFile) - RouteMatcher.openMedia(activity, url) - } - override fun onPageStartedCallback(webView: WebView, url: String) = Unit - override fun onPageFinishedCallback(webView: WebView, url: String) = Unit - } - - webView.addVideoClient(requireActivity()) - } - - @Suppress("unused") - private inner class JSDiscussionInterface { - - @Suppress("UNUSED_PARAMETER") - @JavascriptInterface - fun onItemPressed(id: String) { - //do nothing for now - } - - @JavascriptInterface - fun onAvatarPressed(id: String) { - presenter.findEntry(id.toLong())?.let { entry -> - val bundle = StudentContextFragment.makeBundle(entry.author!!.id, canvasContext.id) - RouteMatcher.route(requireActivity(), Route(StudentContextFragment::class.java, null, bundle)) - } - } - - @JavascriptInterface - fun onAttachmentPressed(id: String) { - val entry = presenter.findEntry(id.toLong()) - if(entry != null && !entry.attachments.isNullOrEmpty()) { - viewAttachments(entry.attachments!!) - } - } - - @JavascriptInterface - fun onReplyPressed(id: String) { - showReplyView(id.toLong()) - } - - @JavascriptInterface - fun onMenuPressed(id: String) { - showOverflowMenu(id.toLong()) - } - - @JavascriptInterface - fun onLikePressed(id: String) { - presenter.likeDiscussionPressed(id.toLong()) - } - - @JavascriptInterface - fun onMoreRepliesPressed(id: String) { - val args = makeBundle(presenter.discussionTopicHeader, presenter.discussionTopic, id.toLong(), presenter.getSkipId()) - RouteMatcher.route(requireActivity(), Route(null, DiscussionsDetailsFragment::class.java, canvasContext, args)) - } - - @JavascriptInterface - fun getInViewPort(): String { - return presenter.discussionTopic.unreadEntries.joinToString() - } - - @JavascriptInterface - fun inViewPortAndUnread(idList: String) { - if(idList.isNotEmpty()) { - presenter.markAsRead(idList.split(",").map(String::toLong)) - } - } - - @JavascriptInterface - fun getLikedImage(): String { - //Returns a string of a bitmap colored for the thumbs up (like) image. - val likeImage = DiscussionUtils.getBitmapFromAssets(requireContext(), "discussion_liked.png") - return DiscussionUtils.makeBitmapForWebView(ThemePrefs.brandColor, likeImage) - } - - //A helper to log out messages from the JS code - @JavascriptInterface - fun logMessage(message: String) { Logger.d(message) } - - /** - * Calculates the offset of the scrollview and it's content as compared to the elements position within the webview. - * A scrollview's visible window can be between 0 and the size of the scrollview's height. This looks at the content on top - * of the discussion replies webview and adds that to the elements position to come up with a relative position for the element - * within the scrollview. In sort we are finding the elements position within a scrollview. - */ - @Suppress("UNUSED_PARAMETER") - @JavascriptInterface - fun calculateActualOffset(elementId: String, elementHeight: String, elementTopOffset: String): Boolean { - // Javascript passes us back a number, which could be either a float or an int, so we'll need to convert the string first to a float, then an int - return isElementInViewPortWithinScrollView(elementHeight.toFloat().toInt(), elementTopOffset.toFloat().toInt()) - } - } - - override fun updateDiscussionLiked(discussionEntry: DiscussionEntry) { - updateDiscussionLikedState(discussionEntry, "setLiked"/*Constant found in the JS files*/) - } - - override fun updateDiscussionUnliked(discussionEntry: DiscussionEntry) { - updateDiscussionLikedState(discussionEntry, "setUnliked" /*Constant found in the JS files*/) - } - - private fun updateDiscussionLikedState(discussionEntry: DiscussionEntry, methodName: String) = with(binding) { - val likingSum = if(discussionEntry.ratingSum == 0) "" else "(" + discussionEntry.ratingSum + ")" - val likingSumAllyText = DiscussionEntryHtmlConverter.getLikeCountText(requireContext(), discussionEntry) - val likingColor = DiscussionUtils.getHexColorString(if (discussionEntry._hasRated) ThemePrefs.brandColor else ContextCompat.getColor(requireContext(), R.color.textDark)) - requireActivity().runOnUiThread { - discussionRepliesWebViewWrapper.webView.loadUrl("javascript:$methodName('${discussionEntry.id}')") - discussionRepliesWebViewWrapper.webView.loadUrl("javascript:updateLikedCount('${discussionEntry.id}','$likingSum','$likingColor','$likingSumAllyText')") - } - } - - override fun updateDiscussionEntry(discussionEntry: DiscussionEntry) = with(binding) { - requireActivity().runOnUiThread { - discussionRepliesWebViewWrapper.webView.loadUrl("javascript:updateEntry('${discussionEntry.id}', '${discussionEntry.message}')") - if (discussionEntry.attachments == null) - discussionRepliesWebViewWrapper.webView.loadUrl("javascript:hideAttachmentIcon('${discussionEntry.id}'") - } - } - - private fun showReplyView(id: Long) { - if (APIHelper.hasNetworkConnection()) { - val args = DiscussionsReplyFragment.makeBundle(presenter.discussionTopicHeader.id, id, isAnnouncements) - RouteMatcher.route(requireActivity(), Route(DiscussionsReplyFragment::class.java, presenter.canvasContext, args)) - } else { - NoInternetConnectionDialog.show(requireFragmentManager()) - } - } - - private fun markAsUnread(id: Long) { - if (APIHelper.hasNetworkConnection()) { - presenter.markAsUnread(id) - } else { - NoInternetConnectionDialog.show(requireFragmentManager()) - } - } - - private fun showOverflowMenu(id: Long) { - parentFragmentManager.let { - DiscussionBottomSheetMenuFragment.show(it, id) - } - } - - private fun showUpdateReplyView(id: Long) { - if (APIHelper.hasNetworkConnection()) { - val args = DiscussionsUpdateFragment.makeBundle(presenter.discussionTopicHeader.id, presenter.findEntry(id), isAnnouncements, presenter.discussionTopic) - RouteMatcher.route(requireActivity(), Route(DiscussionsUpdateFragment::class.java, presenter.canvasContext, args)) - } else { - NoInternetConnectionDialog.show(requireFragmentManager()) - } - } - - private fun deleteDiscussionEntry(id: Long) { - if (APIHelper.hasNetworkConnection()) { - val builder = AlertDialog.Builder(requireContext()) - builder.setMessage(R.string.discussions_delete_warning) - builder.setPositiveButton(android.R.string.ok) { _, _ -> - presenter.deleteDiscussionEntry(id) - } - builder.setNegativeButton(android.R.string.cancel) { _, _ -> } - val dialog = builder.create() - dialog.setOnShowListener { - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(ThemePrefs.textButtonColor) - dialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(ThemePrefs.textButtonColor) - } - dialog.show() - } else { - NoInternetConnectionDialog.show(requireFragmentManager()) - } - } - - override fun updateDiscussionAsDeleted(discussionEntry: DiscussionEntry) { - val deletedText = DiscussionUtils.formatDeletedInfoText(requireContext(), discussionEntry) - binding.discussionRepliesWebViewWrapper.post { binding.discussionRepliesWebViewWrapper.webView.loadUrl( - "javascript:markAsDeleted" + "('" + discussionEntry.id.toString() + "','" + deletedText + "')") } - } - - override fun updateDiscussionsMarkedAsReadCompleted(markedAsReadIds: List) { - markedAsReadIds.forEach { - binding.discussionRepliesWebViewWrapper.post { binding.discussionRepliesWebViewWrapper.webView.loadUrl("javascript:markAsRead('$it')") } - } - } - - override fun updateDiscussionsMarkedAsUnreadCompleted(markedAsUnreadId: Long) { - binding.discussionRepliesWebViewWrapper.post { binding.discussionRepliesWebViewWrapper.webView.loadUrl("javascript:markAsUnread('$markedAsUnreadId')") } - } - - override fun showAnonymousDiscussionView() = with(binding) { - anonymousDiscussionsNotSupported.setVisible() - openInBrowser.setVisible(discussionTopicHeader.htmlUrl?.isNotEmpty() == true) - replyToDiscussionTopic.setGone() - swipeRefreshLayout.isEnabled = false - openInBrowser.onClick { - discussionTopicHeader.htmlUrl?.let { url -> - requireContext().startActivity(InternalWebViewActivity.createIntent(requireContext(), url, "", true)) - } - } - } - - /** - * Checks to see if the webview element is within the viewable bounds of the scrollview. - */ - private fun isElementInViewPortWithinScrollView(elementHeight: Int, topOffset: Int): Boolean = with(binding) { - val scrollBounds = Rect().apply { discussionsScrollView.getDrawingRect(this) } - - val discussionRepliesHeight = discussionRepliesWebViewWrapper.height - val discussionScrollViewContentHeight = discussionsScrollViewContentWrapper.height - val otherContentHeight = discussionScrollViewContentHeight - discussionRepliesHeight - val top = requireContext().DP(topOffset) + otherContentHeight - val bottom = top + requireContext().DP(elementHeight) - - return scrollBounds.top < top && scrollBounds.bottom > bottom - } - - private fun viewAttachments(remoteFiles: List) { - val attachments = ArrayList() - remoteFiles.forEach { attachments.add(it.mapToAttachment()) } - if (attachments.isNotEmpty()) { - if (attachments.size > 1) { - AttachmentPickerDialog.show(requireFragmentManager(), attachments) { attachment -> - AttachmentPickerDialog.hide(requireFragmentManager()) - attachment.view(requireActivity()) - } - } else { - attachments[0].view(requireActivity()) - } - } - } - - @Suppress("unused") - @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) - fun onDiscussionUpdated(event: DiscussionUpdatedEvent) { - event.once(javaClass.simpleName) { - presenter.discussionTopicHeader = it - populateDiscussionTopicHeader(it, false) - } - } - - @Suppress("unused") - @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) - fun onDiscussionTopicHeaderDeleted(event: DiscussionTopicHeaderDeletedEvent) { - // Depending on the device and where we delete the discussion topic header from we handle this in two places. - // This situation handles when we delete from discussions list, the other found in readySetGo handles the create discussion fragment. - event.once(javaClass.simpleName + ".onPost()") { - if (it == presenter.discussionTopicHeader.id) { - if(activity is MasterDetailInteractions) { - (activity as MasterDetailInteractions).popFragment(canvasContext) - } - } else if(activity is FullScreenInteractions) { - requireActivity().finish() - } - } - } - - @Suppress("unused") - @Subscribe(threadMode = ThreadMode.MAIN) - fun onOverFlowMenuClicked(event: DiscussionOverflowMenuClickedEvent) { - val id = event.entryId - when(event.type) { - DiscussionBottomSheetChoice.MARK_AS_UNREAD -> markAsUnread(id) - DiscussionBottomSheetChoice.EDIT -> showUpdateReplyView(id) - DiscussionBottomSheetChoice.DELETE -> deleteDiscussionEntry(id) - } - } - - @PageViewUrlParam("type") - fun pageViewType(): String = if (isAnnouncements) "announcements" else "discussion_topics" - - companion object { - const val DISCUSSION_TOPIC_HEADER = "discussion_topic_header" - const val DISCUSSION_TOPIC_HEADER_ID = "discussion_topic_header_id" - const val DISCUSSION_TOPIC = "discussion_topic" - const val DISCUSSION_ENTRY_ID = "discussion_entry_id" - private const val SKIP_IDENTITY_CHECK = "skip_identity_check" - private const val IS_NESTED_DETAIL = "is_nested_detail" - private const val SKIP_ID = "skipId" - private const val IS_ANNOUNCEMENT = "is_announcement" - - @JvmStatic fun makeBundle(discussionTopicHeader: DiscussionTopicHeader): Bundle = Bundle().apply { - putParcelable(DISCUSSION_TOPIC_HEADER, discussionTopicHeader) - } - - @JvmStatic fun makeBundle(discussionTopicHeader: DiscussionTopicHeader, isAnnouncement: Boolean): Bundle = Bundle().apply { - putParcelable(DISCUSSION_TOPIC_HEADER, discussionTopicHeader) - putBoolean(IS_ANNOUNCEMENT, isAnnouncement) - } - - @JvmStatic fun makeBundle(discussionTopicHeaderId: Long): Bundle = Bundle().apply { - putLong(DISCUSSION_TOPIC_HEADER_ID, discussionTopicHeaderId) - } - - @JvmStatic fun makeBundle(discussionTopicHeaderId: Long, entryId: Long): Bundle = Bundle().apply { - putLong(DISCUSSION_TOPIC_HEADER_ID, discussionTopicHeaderId) - putLong(DISCUSSION_ENTRY_ID, entryId) - } - - @JvmStatic fun makeBundle( - discussionTopicHeader: DiscussionTopicHeader, - discussionTopic: DiscussionTopic, - discussionEntryId: Long, - skipId: String): Bundle = Bundle().apply { - - // Used for viewing more entries, beyond the default nesting - putParcelable(DISCUSSION_TOPIC_HEADER, discussionTopicHeader) - putParcelable(DISCUSSION_TOPIC, discussionTopic) - putLong(DISCUSSION_ENTRY_ID, discussionEntryId) - putBoolean(SKIP_IDENTITY_CHECK, true) - putBoolean(IS_NESTED_DETAIL, true) - putString(SKIP_ID, skipId) - } - - @JvmStatic fun newInstance(canvasContext: CanvasContext, args: Bundle) = DiscussionsDetailsFragment().withArgs(args).apply { - this.canvasContext = canvasContext - } - } -} diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/routing/DiscussionRouteHelperTeacherRepository.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/routing/DiscussionRouteHelperTeacherRepository.kt index d38ebaad48..fc8957c3d2 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/routing/DiscussionRouteHelperTeacherRepository.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/routing/DiscussionRouteHelperTeacherRepository.kt @@ -9,12 +9,8 @@ import com.instructure.pandautils.features.discussion.router.DiscussionRouteHelp class DiscussionRouteHelperTeacherRepository( private val networkDataSource: DiscussionRouteHelperNetworkDataSource ): DiscussionRouteHelperRepository { - override suspend fun getEnabledFeaturesForCourse( - canvasContext: CanvasContext, - forceNetwork: Boolean - ): Boolean { - return networkDataSource.getEnabledFeaturesForCourse(canvasContext, forceNetwork) - } + + override suspend fun shouldShowDiscussionRedesign(): Boolean = true override suspend fun getDiscussionTopicHeader( canvasContext: CanvasContext, diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/routing/TeacherDiscussionRouter.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/routing/TeacherDiscussionRouter.kt index d278a1e5e8..676b6b340d 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/routing/TeacherDiscussionRouter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/discussion/routing/TeacherDiscussionRouter.kt @@ -4,11 +4,9 @@ import androidx.fragment.app.FragmentActivity import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.models.Group -import com.instructure.interactions.router.Route import com.instructure.pandautils.features.discussion.details.DiscussionDetailsWebViewFragment import com.instructure.pandautils.features.discussion.router.DiscussionRouter import com.instructure.teacher.activities.FullscreenActivity -import com.instructure.teacher.features.discussion.DiscussionsDetailsFragment import com.instructure.teacher.router.RouteMatcher class TeacherDiscussionRouter(private val activity: FragmentActivity) : DiscussionRouter { @@ -18,16 +16,7 @@ class TeacherDiscussionRouter(private val activity: FragmentActivity) : Discussi discussionTopicHeader: DiscussionTopicHeader, isAnnouncement: Boolean ) { - val route = when { - isRedesign -> DiscussionDetailsWebViewFragment.makeRoute(canvasContext, discussionTopicHeader) - else -> { - val bundle = DiscussionsDetailsFragment.makeBundle( - discussionTopicHeader, - isAnnouncement || discussionTopicHeader.announcement - ) - Route(null, DiscussionsDetailsFragment::class.java, canvasContext, bundle) - } - } + val route = DiscussionDetailsWebViewFragment.makeRoute(canvasContext, discussionTopicHeader) route.apply { removePreviousScreen = true diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/progression/ModuleProgressionFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/progression/ModuleProgressionFragment.kt index 2a1dbca054..827c6932e7 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/progression/ModuleProgressionFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/progression/ModuleProgressionFragment.kt @@ -38,9 +38,8 @@ import com.instructure.pandautils.utils.makeBundle import com.instructure.pandautils.utils.setHidden import com.instructure.teacher.R import com.instructure.teacher.databinding.FragmentModuleProgressionBinding -import com.instructure.teacher.features.discussion.DiscussionsDetailsFragment -import com.instructure.teacher.features.files.details.FileDetailsFragment import com.instructure.teacher.features.assignment.details.AssignmentDetailsFragment +import com.instructure.teacher.features.files.details.FileDetailsFragment import com.instructure.teacher.fragments.InternalWebViewFragment import com.instructure.teacher.fragments.PageDetailsFragment import com.instructure.teacher.fragments.QuizDetailsFragment @@ -115,15 +114,9 @@ class ModuleProgressionFragment : Fragment() { canvasContext as Course, AssignmentDetailsFragment.makeBundle(item.assignmentId) ) - is ModuleItemViewData.Discussion -> if (item.isDiscussionRedesignEnabled) { - DiscussionDetailsWebViewFragment.newInstance( - DiscussionDetailsWebViewFragment.makeRoute(canvasContext, item.discussionTopicHeaderId) - )!! - } else { - DiscussionsDetailsFragment.newInstance( - canvasContext, DiscussionsDetailsFragment.makeBundle(item.discussionTopicHeaderId) - ) - } + is ModuleItemViewData.Discussion -> DiscussionDetailsWebViewFragment.newInstance( + DiscussionDetailsWebViewFragment.makeRoute(canvasContext, item.discussionTopicHeaderId) + )!! is ModuleItemViewData.Quiz -> QuizDetailsFragment.newInstance( canvasContext as Course, QuizDetailsFragment.makeBundle(item.quizId) diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/progression/ModuleProgressionViewData.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/progression/ModuleProgressionViewData.kt index 4b0ae5409f..c928fc68c6 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/progression/ModuleProgressionViewData.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/progression/ModuleProgressionViewData.kt @@ -33,7 +33,7 @@ sealed class ModuleProgressionAction { sealed class ModuleItemViewData { data class Page(val pageUrl: String) : ModuleItemViewData() data class Assignment(val assignmentId: Long) : ModuleItemViewData() - data class Discussion(val isDiscussionRedesignEnabled: Boolean, val discussionTopicHeaderId: Long) : ModuleItemViewData() + data class Discussion(val discussionTopicHeaderId: Long) : ModuleItemViewData() data class Quiz(val quizId: Long) : ModuleItemViewData() data class External(val url: String, val title: String) : ModuleItemViewData() data class File(val fileUrl: String) : ModuleItemViewData() diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/progression/ModuleProgressionViewModel.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/progression/ModuleProgressionViewModel.kt index 4238c96910..715925b2aa 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/modules/progression/ModuleProgressionViewModel.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/modules/progression/ModuleProgressionViewModel.kt @@ -75,13 +75,11 @@ class ModuleProgressionViewModel @Inject constructor( } } - val isDiscussionRedesignEnabled = discussionRouteHelperRepository.getEnabledFeaturesForCourse(canvasContext, true) - val modules = repository.getModulesWithItems(canvasContext) val items = modules .flatMap { it.items } - .map { createModuleItemViewData(it, isDiscussionRedesignEnabled) to it } + .map { createModuleItemViewData(it) to it } .filter { it.first != null } val position = if (currentPosition == -1) { @@ -110,10 +108,10 @@ class ModuleProgressionViewModel @Inject constructor( } } - private fun createModuleItemViewData(item: ModuleItem, isDiscussionRedesignEnabled: Boolean) = when (item.type) { + private fun createModuleItemViewData(item: ModuleItem) = when (item.type) { Type.Page.name -> ModuleItemViewData.Page(item.pageUrl.orEmpty()) Type.Assignment.name -> ModuleItemViewData.Assignment(item.contentId) - Type.Discussion.name -> ModuleItemViewData.Discussion(isDiscussionRedesignEnabled, item.contentId) + Type.Discussion.name -> ModuleItemViewData.Discussion(item.contentId) Type.Quiz.name -> ModuleItemViewData.Quiz(item.contentId) Type.ExternalUrl.name, Type.ExternalTool.name -> { val url = Uri.parse(item.htmlUrl).buildUpon().appendQueryParameter("display", "borderless").build().toString() diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/DiscussionsListFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/DiscussionsListFragment.kt index 9faf698d3e..f306dad082 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/DiscussionsListFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/DiscussionsListFragment.kt @@ -29,6 +29,7 @@ import com.instructure.interactions.router.Route import com.instructure.pandautils.analytics.SCREEN_VIEW_DISCUSSION_LIST import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.binding.viewBinding +import com.instructure.pandautils.features.discussion.details.DiscussionDetailsWebViewFragment import com.instructure.pandautils.fragments.BaseExpandableSyncFragment import com.instructure.pandautils.utils.* import com.instructure.teacher.R @@ -37,7 +38,6 @@ import com.instructure.teacher.databinding.FragmentDiscussionListBinding import com.instructure.teacher.dialog.DiscussionsMoveToDialog import com.instructure.teacher.events.* import com.instructure.teacher.factory.DiscussionListPresenterFactory -import com.instructure.teacher.features.discussion.DiscussionsDetailsFragment import com.instructure.teacher.presenters.DiscussionListPresenter import com.instructure.teacher.router.RouteMatcher import com.instructure.teacher.utils.RecyclerViewUtils @@ -239,7 +239,7 @@ open class DiscussionsListFragment : BaseExpandableSyncFragment< } override fun discussionDeletedSuccessfully(discussionTopicHeader: DiscussionTopicHeader) { - DiscussionTopicHeaderDeletedEvent(discussionTopicHeader.id, (DiscussionsDetailsFragment::class.java.toString() + ".onPost()")).post() + DiscussionTopicHeaderDeletedEvent(discussionTopicHeader.id, (DiscussionDetailsWebViewFragment::class.java.toString() + ".onPost()")).post() } override fun displayLoadingError() = toast(R.string.errorOccurred) diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/DiscussionsReplyFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/DiscussionsReplyFragment.kt deleted file mode 100644 index 048b6180d6..0000000000 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/DiscussionsReplyFragment.kt +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright (C) 2017 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ -package com.instructure.teacher.fragments - -import android.app.Activity -import android.content.Intent -import android.os.Bundle -import android.view.LayoutInflater -import android.view.MenuItem -import android.view.WindowManager -import com.instructure.canvasapi2.models.CanvasContext -import com.instructure.canvasapi2.models.DiscussionEntry -import com.instructure.canvasapi2.models.postmodels.FileSubmitObject -import com.instructure.canvasapi2.utils.APIHelper -import com.instructure.canvasapi2.utils.Logger -import com.instructure.pandautils.analytics.SCREEN_VIEW_DISCUSSIONS_REPLY -import com.instructure.pandautils.analytics.ScreenView -import com.instructure.pandautils.features.file.upload.FileUploadDialogFragment -import com.instructure.pandautils.features.file.upload.FileUploadDialogParent -import com.instructure.pandautils.fragments.BasePresenterFragment -import com.instructure.pandautils.utils.LongArg -import com.instructure.pandautils.utils.MediaUploadUtils -import com.instructure.pandautils.utils.ParcelableArg -import com.instructure.pandautils.utils.RequestCodes -import com.instructure.pandautils.utils.ViewStyler -import com.instructure.pandautils.utils.toast -import com.instructure.pandautils.views.AttachmentView -import com.instructure.teacher.R -import com.instructure.teacher.databinding.FragmentDiscussionsReplyBinding -import com.instructure.teacher.dialog.NoInternetConnectionDialog -import com.instructure.teacher.events.DiscussionEntryEvent -import com.instructure.teacher.events.post -import com.instructure.teacher.factory.DiscussionsReplyFactory -import com.instructure.teacher.presenters.DiscussionsReplyPresenter -import com.instructure.teacher.presenters.DiscussionsReplyPresenter.Companion.REASON_MESSAGE_EMPTY -import com.instructure.teacher.presenters.DiscussionsReplyPresenter.Companion.REASON_MESSAGE_FAILED_TO_SEND -import com.instructure.teacher.presenters.DiscussionsReplyPresenter.Companion.REASON_MESSAGE_IN_PROGRESS -import com.instructure.teacher.utils.setupCloseButton -import com.instructure.teacher.utils.setupMenu -import com.instructure.teacher.viewinterface.DiscussionsReplyView - -@ScreenView(SCREEN_VIEW_DISCUSSIONS_REPLY) -class DiscussionsReplyFragment : BasePresenterFragment< - DiscussionsReplyPresenter, - DiscussionsReplyView, - FragmentDiscussionsReplyBinding>(), - DiscussionsReplyView, - FileUploadDialogParent { - - private var mCanvasContext: CanvasContext by ParcelableArg(default = CanvasContext.getGenericContext(CanvasContext.Type.COURSE, -1L, "")) - private var mDiscussionTopicHeaderId: Long by LongArg(default = 0L) // The topic the discussion belongs too - private var mDiscussionEntryId: Long by LongArg(default = 0L) // The future parent of the discussion entry we are creating - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setHasOptionsMenu(true) - activity?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE) - } - - override fun onRefreshFinished() {} - override fun onRefreshStarted() {} - - override val bindingInflater: (layoutInflater: LayoutInflater) -> FragmentDiscussionsReplyBinding = FragmentDiscussionsReplyBinding::inflate - - override fun getPresenterFactory() = DiscussionsReplyFactory(mCanvasContext, mDiscussionTopicHeaderId, mDiscussionEntryId) - - override fun onPresenterPrepared(presenter: DiscussionsReplyPresenter) {} - - override fun onReadySetGo(presenter: DiscussionsReplyPresenter) = with(binding) { - rceTextEditor.setHint(R.string.rce_empty_message) - rceTextEditor.requestEditorFocus() - rceTextEditor.showEditorToolbar() - rceTextEditor.actionUploadImageCallback = { MediaUploadUtils.showPickImageDialog(this@DiscussionsReplyFragment) } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (resultCode == Activity.RESULT_OK) { - // Get the image Uri - when (requestCode) { - RequestCodes.PICK_IMAGE_GALLERY -> data?.data - RequestCodes.CAMERA_PIC_REQUEST -> MediaUploadUtils.handleCameraPicResult(requireActivity(), null) - else -> null - }?.let { imageUri -> - presenter.uploadRceImage(imageUri, requireActivity()) - } - } - } - - override fun messageSuccess(entry: DiscussionEntry) { - DiscussionEntryEvent(entry).post() - requireActivity().onBackPressed() - toast(R.string.discussion_sent_success) - } - - override fun messageFailure(reason: Int) { - when (reason) { - REASON_MESSAGE_IN_PROGRESS -> { - Logger.e("User tried to send message multiple times in a row.") - } - REASON_MESSAGE_EMPTY -> { - Logger.e("User tried to send message an empty message.") - toast(R.string.discussion_sent_empty) - } - REASON_MESSAGE_FAILED_TO_SEND -> { - Logger.e("Message failed to send for some reason.") - toast(R.string.discussion_sent_failure) - } - } - } - - override fun onResume() { - super.onResume() - setupToolbar() - } - - private fun setupToolbar() = with(binding) { - toolbar.title = getString(R.string.reply) - toolbar.setupCloseButton(this@DiscussionsReplyFragment) - toolbar.setupMenu(R.menu.menu_discussion_reply, menuItemCallback) - - ViewStyler.themeToolbarLight(requireActivity(), toolbar) - ViewStyler.setToolbarElevationSmall(requireContext(), toolbar) - } - - private val menuItemCallback: (MenuItem) -> Unit = { item -> - when (item.itemId) { - R.id.menu_send -> { - if (APIHelper.hasNetworkConnection()) { - presenter.sendMessage(binding.rceTextEditor.html) - } else { - NoInternetConnectionDialog.show(requireFragmentManager()) - } - } - R.id.menu_attachment -> { - if (APIHelper.hasNetworkConnection()) { - val attachments = ArrayList() - if (presenter.getAttachment() != null) { - attachments.add(presenter.getAttachment()!!) - } - - val bundle = FileUploadDialogFragment.createDiscussionsBundle(attachments) - FileUploadDialogFragment.newInstance(bundle).show(childFragmentManager, FileUploadDialogFragment.TAG) - } else { - NoInternetConnectionDialog.show(requireFragmentManager()) - } - } - } - } - - override fun attachmentCallback(event: Int, attachment: FileSubmitObject?) { - if (event == FileUploadDialogFragment.EVENT_ON_FILE_SELECTED) { - applyAttachment(attachment) - } - } - - private fun applyAttachment(file: FileSubmitObject?) { - if (file != null) { - presenter.setAttachment(file) - binding.attachments.setAttachment(file.toAttachment()) { action, _ -> - if (action == AttachmentView.AttachmentAction.REMOVE) { - presenter.setAttachment(null) - } - } - } - } - - override fun insertImageIntoRCE(imageUrl: String) = binding.rceTextEditor.insertImage(requireActivity(), imageUrl) - - companion object { - private const val DISCUSSION_TOPIC_HEADER_ID = "DISCUSSION_TOPIC_HEADER_ID" - private const val DISCUSSION_ENTRY_ID = "DISCUSSION_ENTRY_ID" - private const val IS_ANNOUNCEMENT = "IS_ANNOUNCEMENT" - - fun makeBundle(discussionTopicHeaderId: Long, discussionEntryId: Long, isAnnouncement: Boolean): Bundle = - Bundle().apply { - putLong(DISCUSSION_TOPIC_HEADER_ID, discussionTopicHeaderId) - putLong(DISCUSSION_ENTRY_ID, discussionEntryId) - putBoolean(IS_ANNOUNCEMENT, isAnnouncement) - } - - fun newInstance(canvasContext: CanvasContext, args: Bundle) = - DiscussionsReplyFragment().apply { - mDiscussionTopicHeaderId = args.getLong(DISCUSSION_TOPIC_HEADER_ID) - mDiscussionEntryId = args.getLong(DISCUSSION_ENTRY_ID) - mCanvasContext = canvasContext - } - } -} diff --git a/apps/teacher/src/main/java/com/instructure/teacher/presenters/CreateDiscussionPresenter.kt b/apps/teacher/src/main/java/com/instructure/teacher/presenters/CreateDiscussionPresenter.kt index c92cdb2341..050a27ba27 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/presenters/CreateDiscussionPresenter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/presenters/CreateDiscussionPresenter.kt @@ -28,10 +28,10 @@ import com.instructure.canvasapi2.utils.weave.WeaveJob import com.instructure.canvasapi2.utils.weave.awaitApi import com.instructure.canvasapi2.utils.weave.weave import com.instructure.canvasapi2.models.postmodels.FileSubmitObject +import com.instructure.pandautils.features.discussion.details.DiscussionDetailsWebViewFragment import com.instructure.pandautils.utils.MediaUploadUtils import com.instructure.teacher.events.DiscussionTopicHeaderDeletedEvent import com.instructure.teacher.events.post -import com.instructure.teacher.features.discussion.DiscussionsDetailsFragment import com.instructure.teacher.interfaces.RceMediaUploadPresenter import com.instructure.teacher.viewinterface.CreateDiscussionView import instructure.androidblueprint.FragmentPresenter @@ -138,7 +138,7 @@ class CreateDiscussionPresenter(private val canvasContext: CanvasContext, privat DiscussionManager.deleteDiscussionTopicHeader(canvasContext, discussionTopicHeaderId, object : StatusCallback() { override fun onResponse(response: Response, linkHeaders: LinkHeaders, type: ApiType) { if (response.code() in 200..299) { - DiscussionTopicHeaderDeletedEvent(discussionTopicHeaderId, (DiscussionsDetailsFragment::class.java.toString() + ".onResume()")).post() + DiscussionTopicHeaderDeletedEvent(discussionTopicHeaderId, (DiscussionDetailsWebViewFragment::class.java.toString() + ".onResume()")).post() viewCallback?.discussionDeletedSuccessfully(discussionTopicHeaderId) } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/presenters/CreateOrEditAnnouncementPresenter.kt b/apps/teacher/src/main/java/com/instructure/teacher/presenters/CreateOrEditAnnouncementPresenter.kt index a0c7b93ab7..48bdf05162 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/presenters/CreateOrEditAnnouncementPresenter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/presenters/CreateOrEditAnnouncementPresenter.kt @@ -30,12 +30,12 @@ import com.instructure.canvasapi2.utils.weave.awaitApi import com.instructure.canvasapi2.utils.weave.catch import com.instructure.canvasapi2.utils.weave.tryWeave import com.instructure.canvasapi2.models.postmodels.FileSubmitObject +import com.instructure.pandautils.features.discussion.details.DiscussionDetailsWebViewFragment import com.instructure.pandautils.utils.MediaUploadUtils import com.instructure.teacher.events.DiscussionCreatedEvent import com.instructure.teacher.events.DiscussionTopicHeaderDeletedEvent import com.instructure.teacher.events.DiscussionUpdatedEvent import com.instructure.teacher.events.post -import com.instructure.teacher.features.discussion.DiscussionsDetailsFragment import com.instructure.teacher.interfaces.RceMediaUploadPresenter import com.instructure.teacher.viewinterface.CreateOrEditAnnouncementView import instructure.androidblueprint.FragmentPresenter @@ -133,8 +133,8 @@ class CreateOrEditAnnouncementPresenter( fun deleteAnnouncement() { viewCallback?.onSaveStarted() apiJob = tryWeave { - awaitApi { DiscussionManager.deleteDiscussionTopicHeader(canvasContext, announcement.id, it) } - DiscussionTopicHeaderDeletedEvent(announcement.id, (DiscussionsDetailsFragment::class.java.toString() + ".onResume()")).post() + awaitApi { DiscussionManager.deleteDiscussionTopicHeader(canvasContext, announcement.id, it) } + DiscussionTopicHeaderDeletedEvent(announcement.id, (DiscussionDetailsWebViewFragment::class.java.toString() + ".onResume()")).post() viewCallback?.onDeleteSuccess() } catch { viewCallback?.onDeleteError() diff --git a/apps/teacher/src/main/java/com/instructure/teacher/presenters/DiscussionsDetailsPresenter.kt b/apps/teacher/src/main/java/com/instructure/teacher/presenters/DiscussionsDetailsPresenter.kt deleted file mode 100644 index 58e8205d6d..0000000000 --- a/apps/teacher/src/main/java/com/instructure/teacher/presenters/DiscussionsDetailsPresenter.kt +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Copyright (C) 2017 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.instructure.teacher.presenters - -import com.instructure.canvasapi2.StatusCallback -import com.instructure.canvasapi2.managers.DiscussionManager -import com.instructure.canvasapi2.managers.SubmissionManager -import com.instructure.canvasapi2.models.* -import com.instructure.canvasapi2.utils.ApiPrefs -import com.instructure.canvasapi2.utils.ApiType -import com.instructure.canvasapi2.utils.LinkHeaders -import com.instructure.canvasapi2.utils.weave.* -import com.instructure.teacher.events.DiscussionTopicEvent -import com.instructure.teacher.events.DiscussionTopicHeaderEvent -import com.instructure.teacher.events.post -import com.instructure.teacher.viewinterface.DiscussionsDetailsView -import instructure.androidblueprint.FragmentPresenter -import kotlinx.coroutines.Job -import retrofit2.Response -import java.util.* - -class DiscussionsDetailsPresenter( - var canvasContext: CanvasContext, - var discussionTopicHeader: DiscussionTopicHeader, - var discussionTopic: DiscussionTopic, - val discussionSkipId: String) : FragmentPresenter() { - - var scrollPosition: Int = 0 - var discussionEntryInitJob: Job? = null - - private var mApiCalls: Job? = null - private var discussionEntryRatingCallback: StatusCallback? = null - private var mDiscussionMarkAsReadApiCalls: Job? = null - private var mDiscussionMarkAsUnreadApiCalls: Job? = null - - override fun loadData(forceNetwork: Boolean) { - viewCallback?.onRefreshStarted() - if (discussionTopicHeader.anonymousState == null) { - DiscussionManager.getFullDiscussionTopic(canvasContext, discussionTopicHeader.id, forceNetwork, mDiscussionTopicCallback) - } else { - viewCallback?.onRefreshFinished() - viewCallback?.showAnonymousDiscussionView() - } - } - - override fun refresh(forceNetwork: Boolean) { - mDiscussionTopicCallback.reset() - } - - @Suppress("EXPERIMENTAL_FEATURE_WARNING") - fun getSubmissionData(forceNetwork: Boolean) { - mApiCalls?.cancel() - mApiCalls = weave { - val assignment = discussionTopicHeader.assignment - try { - val submissionSummary = awaitApi { SubmissionManager.getSubmissionSummary(assignment!!.courseId, assignment.id, forceNetwork, it) } - val totalStudents = submissionSummary.graded + submissionSummary.ungraded + submissionSummary.notSubmitted - viewCallback?.updateSubmissionDonuts(totalStudents, submissionSummary.graded, submissionSummary.ungraded, submissionSummary.notSubmitted) - } catch (ignore: Throwable) { - } - } - } - - @Suppress("EXPERIMENTAL_FEATURE_WARNING") - fun getDiscussionTopicHeader(discussionTopicHeaderId: Long) { - DiscussionManager.getDetailedDiscussion(canvasContext, discussionTopicHeaderId, object: StatusCallback() { - override fun onResponse(response: Response, linkHeaders: LinkHeaders, type: ApiType) { - response.body()?.let { - discussionTopicHeader = it - viewCallback?.populateDiscussionTopicHeader(discussionTopicHeader, false) - } - } - }, false) - } - - fun updateDiscussionEntryToDiscussionTopic(updatedEntry: DiscussionEntry) { - val entry = findEntry(updatedEntry.id) - entry?.message = updatedEntry.message - entry?.attachments = updatedEntry.attachments - DiscussionTopicEvent(discussionTopic, getSkipId()).post() - } - - fun addDiscussionEntryToDiscussionTopic(newEntry: DiscussionEntry) { - // If for some reason, typically due to lifecycle cancel any API work being done - mDiscussionTopicCallback.cancel() - viewCallback?.onRefreshFinished() - - if(newEntry.author == null) { - setAuthorAsSelf(newEntry) - } - - if(newEntry.parentId == -1L) { //No Parent, add to DiscussionTopicHeader - discussionTopic.views.add(newEntry) - notifyEntryAdded(topLevel = true) - return - } - - discussionEntryInitJob = weave { - // Find the parent, add it to the list of views - inBackground { - discussionTopic.views.forEach { discussionEntry -> - if (discussionEntry.id == newEntry.parentId) { - newEntry.init(discussionTopic, discussionEntry) - discussionEntry.addReply(newEntry) - discussionEntry.totalChildren += 1 - notifyEntryAdded() - } else { - val parentEntry = recursiveFind(newEntry.parentId, discussionEntry.replies!!.toList()) - if (parentEntry != null) { - newEntry.init(discussionTopic, parentEntry) - parentEntry.addReply(newEntry) - parentEntry.totalChildren += 1 - notifyEntryAdded() - } - } - } - } - } - } - - private fun notifyEntryAdded(topLevel: Boolean = false) { - viewCallback?.populateDiscussionTopic(discussionTopicHeader, discussionTopic, topLevel) - // Removed due to issue with reloading discussions after reply/edit, removal doesn't appear to have any negative effects. - //DiscussionTopicEvent(discussionTopic, getSkipId()).post() - - discussionTopicHeader.incrementDiscussionSubentryCount() //Update subentry count - discussionTopicHeader.lastReplyDate?.time = Date().time //Update last post time - DiscussionTopicHeaderEvent(discussionTopicHeader).post() - } - - private val mDiscussionTopicCallback = object: StatusCallback(){ - override fun onResponse(response: Response, linkHeaders: LinkHeaders, type: ApiType) { - viewCallback?.onRefreshFinished() - if(response.code() == 403) { - //forbidden - viewCallback?.populateAsForbidden() - } else { - if(discussionEntryInitJob?.isActive == true) discussionEntryInitJob?.cancel() - discussionEntryInitJob = weave { - response.body()?.let { - discussionTopic = it - inBackground { - discussionTopic.views.forEach { it.init(discussionTopic, it) } - } - viewCallback?.populateDiscussionTopic(discussionTopicHeader, discussionTopic) - } - } - } - } - } - - private fun recursiveFind(startEntryId: Long, replies: List?): DiscussionEntry? { - replies?.forEach { - if(it.id == startEntryId) { - return it - } else { - val items = recursiveFind(startEntryId, it.replies?.toList()) - if(items != null) { - return items - } - } - } - return null - } - - fun findEntry(entryId: Long): DiscussionEntry? { - discussionTopic.views.forEach { discussionEntry -> - if(discussionEntry.id == entryId) { - return discussionEntry - } - - val entry = recursiveFind(entryId, discussionEntry.replies?.toList()) - if(entry != null) { - return entry - } - } - return null - } - - private fun setAuthorAsSelf(discussionEntry: DiscussionEntry) { - val user = ApiPrefs.user - if(user != null) { - val dp = DiscussionParticipant( - id = user.id, - avatarImageUrl = user.avatarUrl, - displayName = user.name, - pronouns = user.pronouns, - htmlUrl = "" - ) - discussionEntry.author = dp - } - } - - fun updateDiscussionTopic(discussionTopic: DiscussionTopic) { - this.discussionTopic = discussionTopic - viewCallback?.populateDiscussionTopic(this.discussionTopicHeader, this.discussionTopic) - } - - fun likeDiscussionPressed(id: Long) { - if(discussionEntryRatingCallback != null && discussionEntryRatingCallback!!.isCallInProgress) return - - val entry = findEntry(id) - if(entry != null) { - //By default users ratings are 0. If they click and no entry rating exits then they have not rated and are 'liking' a post. - val rating = if(discussionTopic.entryRatings.containsKey(id)) discussionTopic.entryRatings[id] ?: 0 else 0 - val newRating = if(rating == 1) 0 else 1 - discussionEntryRatingCallback = object: StatusCallback() { - override fun onResponse(response: Response, linkHeaders: LinkHeaders, type: ApiType) { - if(response.code() in 200..299) { - discussionTopic.entryRatings.put(id, newRating) - - if(newRating == 1) { - entry.ratingSum += 1 - entry._hasRated = true - viewCallback?.updateDiscussionLiked(entry) - } else if(entry.ratingSum > 0) { - entry.ratingSum -= 1 - entry._hasRated = false - viewCallback?.updateDiscussionUnliked(entry) - } - } - } - } - DiscussionManager.rateDiscussionEntry(canvasContext, discussionTopicHeader.id, id, newRating, discussionEntryRatingCallback!!) - } - } - - @Suppress("EXPERIMENTAL_FEATURE_WARNING") - fun markAsRead(ids: List) { - if(mDiscussionMarkAsReadApiCalls != null && mDiscussionMarkAsReadApiCalls!!.isActive) return - mDiscussionMarkAsReadApiCalls = tryWeave { - val markedAsReadIds: MutableList = ArrayList() - ids.forEach { entryId -> - val response = awaitApiResponse{ DiscussionManager.markDiscussionTopicEntryRead(canvasContext, discussionTopicHeader.id, entryId, it) } - if(response.isSuccessful) { - markedAsReadIds.add(entryId) - val entry = findEntry(entryId) - entry?.unread = false - discussionTopic.unreadEntriesMap.remove(entryId) - discussionTopic.unreadEntries.remove(entryId) - if (discussionTopicHeader.unreadCount > 0) discussionTopicHeader.unreadCount -= 1 - } - } - - viewCallback?.updateDiscussionsMarkedAsReadCompleted(markedAsReadIds) - DiscussionTopicHeaderEvent(discussionTopicHeader).post() - } catch { - // Do nothing - } - } - - @Suppress("EXPERIMENTAL_FEATURE_WARNING") - fun markAsUnread(id: Long) { - if (mDiscussionMarkAsUnreadApiCalls != null && mDiscussionMarkAsUnreadApiCalls!!.isActive) return - mDiscussionMarkAsUnreadApiCalls = tryWeave { - var markedAsUnreadId: Long = -1 - - val response = awaitApiResponse{ DiscussionManager.markDiscussionTopicEntryUnread(canvasContext, discussionTopicHeader.id, id, it) } - if (response.isSuccessful) { - markedAsUnreadId = id - val entry = findEntry(id) - entry?.unread = true - discussionTopic.unreadEntriesMap[id] = true - discussionTopic.unreadEntries.add(id) - discussionTopicHeader.unreadCount += 1 - } - - viewCallback?.updateDiscussionsMarkedAsUnreadCompleted(markedAsUnreadId) - DiscussionTopicHeaderEvent(discussionTopicHeader).post() - } catch { - // Do nothing - } - } - - fun deleteDiscussionEntry(entryId: Long) { - DiscussionManager.deleteDiscussionEntry(canvasContext, discussionTopicHeader.id, entryId, object: StatusCallback() { - override fun onResponse(response: Response, linkHeaders: LinkHeaders, type: ApiType) { - if(response.code() in 200..299) { - val entry = findEntry(entryId) - if (entry != null) { - entry.deleted = true - viewCallback?.updateDiscussionAsDeleted(entry) - discussionTopicHeader.decrementDiscussionSubentryCount() - DiscussionTopicHeaderEvent(discussionTopicHeader).post() - } - } - } - }) - } - - /** - * Generates a skip id unique per fragment. That way when a new item is added things don't get called multiple times. - * However, fragments already on the stack will still get the event as it won't be skipped. This keeps the DiscussionTopic - * updated for older fragments on the stack who need to know about the changes. - */ - fun getSkipId(): String { - return discussionSkipId - } - - override fun onDestroyed() { - super.onDestroyed() - mDiscussionMarkAsReadApiCalls?.cancel() - mDiscussionMarkAsUnreadApiCalls?.cancel() - discussionEntryInitJob?.cancel() - } -} diff --git a/apps/teacher/src/main/java/com/instructure/teacher/presenters/DiscussionsReplyPresenter.kt b/apps/teacher/src/main/java/com/instructure/teacher/presenters/DiscussionsReplyPresenter.kt deleted file mode 100644 index aa115dc331..0000000000 --- a/apps/teacher/src/main/java/com/instructure/teacher/presenters/DiscussionsReplyPresenter.kt +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (C) 2017 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ -package com.instructure.teacher.presenters - -import android.app.Activity -import android.net.Uri -import com.instructure.canvasapi2.managers.DiscussionManager -import com.instructure.canvasapi2.models.CanvasContext -import com.instructure.canvasapi2.models.DiscussionEntry -import com.instructure.canvasapi2.utils.weave.WeaveJob -import com.instructure.canvasapi2.utils.weave.awaitApiResponse -import com.instructure.canvasapi2.utils.weave.catch -import com.instructure.canvasapi2.utils.weave.tryWeave -import com.instructure.pandautils.discussions.DiscussionCaching -import com.instructure.canvasapi2.models.postmodels.FileSubmitObject -import com.instructure.pandautils.utils.MediaUploadUtils -import com.instructure.teacher.interfaces.RceMediaUploadPresenter -import com.instructure.teacher.viewinterface.DiscussionsReplyView -import instructure.androidblueprint.FragmentPresenter -import kotlinx.coroutines.Job -import retrofit2.Response -import java.io.File - -class DiscussionsReplyPresenter( - val canvasContext: CanvasContext, - val discussionTopicHeaderId: Long, - private val discussionEntryId: Long) : FragmentPresenter(), RceMediaUploadPresenter { - override var rceImageUploadJob: WeaveJob? = null - - private var postDiscussionJob: Job? = null - - private var attachment: FileSubmitObject? = null - - override fun loadData(forceNetwork: Boolean) {} - override fun refresh(forceNetwork: Boolean) {} - - fun sendMessage(message: String?) { - if(postDiscussionJob?.isActive == true) { - viewCallback?.messageFailure(REASON_MESSAGE_IN_PROGRESS) - return - } - - if(message == null) { - viewCallback?.messageFailure(REASON_MESSAGE_EMPTY) - } else { - postDiscussionJob = tryWeave { - if (attachment == null) { - if (discussionEntryId == discussionTopicHeaderId) { - messageSentResponse(awaitApiResponse { DiscussionManager.postToDiscussionTopic(canvasContext, discussionTopicHeaderId, message, it) }) - } else { - messageSentResponse(awaitApiResponse { DiscussionManager.replyToDiscussionEntry(canvasContext, discussionTopicHeaderId, discussionEntryId, message, it) }) - } - } else { - if (discussionEntryId == discussionTopicHeaderId) { - messageSentResponse(awaitApiResponse { DiscussionManager.postToDiscussionTopic(canvasContext, discussionTopicHeaderId, message, File(attachment!!.fullPath), attachment?.contentType ?: "multipart/form-data", it) }) - } else { - messageSentResponse(awaitApiResponse { DiscussionManager.replyToDiscussionEntry(canvasContext, discussionTopicHeaderId, discussionEntryId, message, File(attachment!!.fullPath), attachment?.contentType ?: "multipart/form-data", it) }) - } - } - } catch { } - } - } - - private fun messageSentResponse(response: Response) { - if (response.code() in 200..299) { - response.body()?.let { entry -> - DiscussionCaching(discussionTopicHeaderId).saveEntry(entry) - viewCallback?.messageSuccess(entry) - } - } else { - viewCallback?.messageFailure(REASON_MESSAGE_FAILED_TO_SEND) - } - } - - fun setAttachment(fileSubmitObject: FileSubmitObject?) { - attachment = fileSubmitObject - } - - fun getAttachment(): FileSubmitObject? = attachment - - override fun uploadRceImage(imageUri: Uri, activity: Activity) { - rceImageUploadJob = MediaUploadUtils.uploadRceImageJob(imageUri, canvasContext, activity) { imageUrl -> viewCallback?.insertImageIntoRCE(imageUrl) } - } - - companion object { - const val REASON_MESSAGE_IN_PROGRESS = 1 - const val REASON_MESSAGE_EMPTY = 2 - const val REASON_MESSAGE_FAILED_TO_SEND = 3 - } - - override fun onDestroyed() { - super.onDestroyed() - postDiscussionJob?.cancel() - rceImageUploadJob?.cancel() - } -} diff --git a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt index 96e60aceb8..42938cdbb6 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteMatcher.kt @@ -69,7 +69,6 @@ import com.instructure.teacher.adapters.StudentContextFragment import com.instructure.teacher.features.assignment.details.AssignmentDetailsFragment import com.instructure.teacher.features.assignment.list.AssignmentListFragment import com.instructure.teacher.features.assignment.submission.AssignmentSubmissionListFragment -import com.instructure.teacher.features.discussion.DiscussionsDetailsFragment import com.instructure.teacher.features.modules.list.ui.ModuleListFragment import com.instructure.teacher.features.modules.progression.ModuleProgressionFragment import com.instructure.teacher.features.postpolicies.ui.PostPolicyFragment @@ -88,7 +87,6 @@ import com.instructure.teacher.fragments.CreateOrEditAnnouncementFragment import com.instructure.teacher.fragments.CreateOrEditPageDetailsFragment import com.instructure.teacher.fragments.DashboardFragment import com.instructure.teacher.fragments.DiscussionsListFragment -import com.instructure.teacher.fragments.DiscussionsReplyFragment import com.instructure.teacher.fragments.DiscussionsUpdateFragment import com.instructure.teacher.fragments.DueDatesFragment import com.instructure.teacher.fragments.EditAssignmentDetailsFragment @@ -268,7 +266,6 @@ object RouteMatcher : BaseRouteMatcher() { bottomSheetFragments.add(EditQuizDetailsFragment::class.java) bottomSheetFragments.add(QuizPreviewWebviewFragment::class.java) bottomSheetFragments.add(AddMessageFragment::class.java) - bottomSheetFragments.add(DiscussionsReplyFragment::class.java) bottomSheetFragments.add(DiscussionsUpdateFragment::class.java) bottomSheetFragments.add(ChooseRecipientsFragment::class.java) bottomSheetFragments.add(CreateDiscussionFragment::class.java) @@ -495,7 +492,6 @@ object RouteMatcher : BaseRouteMatcher() { AnnouncementListFragment::class.java.isAssignableFrom(cls) -> fragment = AnnouncementListFragment .newInstance(canvasContext!!) // This needs to be above DiscussionsListFragment because it extends it DiscussionsListFragment::class.java.isAssignableFrom(cls) -> fragment = DiscussionsListFragment.newInstance(canvasContext!!) - DiscussionsDetailsFragment::class.java.isAssignableFrom(cls) -> fragment = getDiscussionDetailsFragment(canvasContext, route) DiscussionDetailsWebViewFragment::class.java.isAssignableFrom(cls) -> fragment = DiscussionDetailsWebViewFragment.newInstance(route) DiscussionRouterFragment::class.java.isAssignableFrom(cls) -> fragment = DiscussionRouterFragment.newInstance(canvasContext!!, route) InboxFragment::class.java.isAssignableFrom(cls) -> fragment = InboxFragment.newInstance(route) @@ -506,8 +502,6 @@ object RouteMatcher : BaseRouteMatcher() { ViewMediaFragment::class.java.isAssignableFrom(cls) -> fragment = ViewMediaFragment.newInstance(route.arguments) ViewHtmlFragment::class.java.isAssignableFrom(cls) -> fragment = ViewHtmlFragment.newInstance(route.arguments) ViewUnsupportedFileFragment::class.java.isAssignableFrom(cls) -> fragment = ViewUnsupportedFileFragment.newInstance(route.arguments) - cls.isAssignableFrom(DiscussionsReplyFragment::class.java) -> fragment = DiscussionsReplyFragment - .newInstance(canvasContext!!, route.arguments) cls.isAssignableFrom(DiscussionsUpdateFragment::class.java) -> fragment = DiscussionsUpdateFragment .newInstance(canvasContext!!, route.arguments) ChooseRecipientsFragment::class.java.isAssignableFrom(cls) -> fragment = ChooseRecipientsFragment.newInstance(route.arguments) @@ -586,27 +580,6 @@ object RouteMatcher : BaseRouteMatcher() { } } - private fun getDiscussionDetailsFragment(canvasContext: CanvasContext?, route: Route): DiscussionsDetailsFragment { - return when { - route.arguments.containsKey(DiscussionsDetailsFragment.DISCUSSION_TOPIC_HEADER) -> DiscussionsDetailsFragment.newInstance( - canvasContext!!, - route.arguments - ) - route.arguments.containsKey(DiscussionsDetailsFragment.DISCUSSION_TOPIC_HEADER_ID) -> { - val discussionTopicHeaderId = route.arguments.getLong(DiscussionsDetailsFragment.DISCUSSION_TOPIC_HEADER_ID) - val args = DiscussionsDetailsFragment.makeBundle(discussionTopicHeaderId) - DiscussionsDetailsFragment.newInstance(canvasContext!!, args) - } - else -> { - // Parse the route to get the discussion id - val discussionTopicHeaderId = route.paramsHash[RouterParams.MESSAGE_ID]?.toLong() ?: 0L - val entryId = route.queryParamsHash[RouterParams.ENTRY_ID]?.toLong() ?: 0L - val args = DiscussionsDetailsFragment.makeBundle(discussionTopicHeaderId, entryId) - DiscussionsDetailsFragment.newInstance(canvasContext!!, args) - } - } - } - fun getClassDisplayName(context: Context, cls: Class?): String { return when { cls == null -> return "" diff --git a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteResolver.kt b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteResolver.kt index c55ec39cb2..725dcd94a5 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/router/RouteResolver.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/router/RouteResolver.kt @@ -24,7 +24,6 @@ import com.instructure.teacher.adapters.StudentContextFragment import com.instructure.teacher.features.assignment.details.AssignmentDetailsFragment import com.instructure.teacher.features.assignment.list.AssignmentListFragment import com.instructure.teacher.features.assignment.submission.AssignmentSubmissionListFragment -import com.instructure.teacher.features.discussion.DiscussionsDetailsFragment import com.instructure.teacher.features.files.search.FileSearchFragment import com.instructure.teacher.features.modules.list.ui.ModuleListFragment import com.instructure.teacher.features.modules.progression.ModuleProgressionFragment @@ -44,7 +43,6 @@ import com.instructure.teacher.fragments.CreateOrEditAnnouncementFragment import com.instructure.teacher.fragments.CreateOrEditPageDetailsFragment import com.instructure.teacher.fragments.DashboardFragment import com.instructure.teacher.fragments.DiscussionsListFragment -import com.instructure.teacher.fragments.DiscussionsReplyFragment import com.instructure.teacher.fragments.DiscussionsUpdateFragment import com.instructure.teacher.fragments.DueDatesFragment import com.instructure.teacher.fragments.EditAssignmentDetailsFragment @@ -158,8 +156,6 @@ object RouteResolver { fragment = AnnouncementListFragment.newInstance(canvasContext!!) } else if (DiscussionsListFragment::class.java.isAssignableFrom(cls)) { fragment = DiscussionsListFragment.newInstance(canvasContext!!) - } else if (DiscussionsDetailsFragment::class.java.isAssignableFrom(cls)) { - fragment = getDiscussionDetailsFragment(canvasContext, route) } else if (DiscussionRouterFragment::class.java.isAssignableFrom(cls)) { fragment = DiscussionRouterFragment.newInstance(canvasContext!!, route) } else if(DiscussionDetailsWebViewFragment::class.java.isAssignableFrom(cls)) { @@ -180,8 +176,6 @@ object RouteResolver { fragment = ViewHtmlFragment.newInstance(route.arguments) } else if (ViewUnsupportedFileFragment::class.java.isAssignableFrom(cls)) { fragment = ViewUnsupportedFileFragment.newInstance(route.arguments) - } else if (cls.isAssignableFrom(DiscussionsReplyFragment::class.java)) { - fragment = DiscussionsReplyFragment.newInstance(canvasContext!!, route.arguments) } else if (cls.isAssignableFrom(DiscussionsUpdateFragment::class.java)) { fragment = DiscussionsUpdateFragment.newInstance(canvasContext!!, route.arguments) } else if (ChooseRecipientsFragment::class.java.isAssignableFrom(cls)) { @@ -314,23 +308,4 @@ object RouteResolver { PageDetailsFragment.newInstance(canvasContext!!, args) } } - - private fun getDiscussionDetailsFragment(canvasContext: CanvasContext?, route: Route): DiscussionsDetailsFragment { - return when { - route.arguments.containsKey(DiscussionsDetailsFragment.DISCUSSION_TOPIC_HEADER) -> DiscussionsDetailsFragment.newInstance(canvasContext!!, route.arguments) - route.arguments.containsKey(DiscussionsDetailsFragment.DISCUSSION_TOPIC_HEADER_ID) -> { - val discussionTopicHeaderId = route.arguments.getLong(DiscussionsDetailsFragment.DISCUSSION_TOPIC_HEADER_ID) - val args = DiscussionsDetailsFragment.makeBundle(discussionTopicHeaderId) - DiscussionsDetailsFragment.newInstance(canvasContext!!, args) - } - else -> { - //parse the route to get the discussion id - val discussionTopicHeaderId = route.paramsHash[RouterParams.MESSAGE_ID]?.toLong() - ?: 0L - val entryId = route.queryParamsHash[RouterParams.ENTRY_ID]?.toLong() ?: 0L - val args = DiscussionsDetailsFragment.makeBundle(discussionTopicHeaderId, entryId) - DiscussionsDetailsFragment.newInstance(canvasContext!!, args) - } - } - } } diff --git a/apps/teacher/src/main/java/com/instructure/teacher/viewinterface/DiscussionsDetailsView.kt b/apps/teacher/src/main/java/com/instructure/teacher/viewinterface/DiscussionsDetailsView.kt deleted file mode 100644 index dddfa4d5dc..0000000000 --- a/apps/teacher/src/main/java/com/instructure/teacher/viewinterface/DiscussionsDetailsView.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2017 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package com.instructure.teacher.viewinterface - -import com.instructure.canvasapi2.models.DiscussionEntry -import com.instructure.canvasapi2.models.DiscussionTopic -import com.instructure.canvasapi2.models.DiscussionTopicHeader -import instructure.androidblueprint.FragmentViewInterface - -interface DiscussionsDetailsView : FragmentViewInterface { - fun populateDiscussionTopic(discussionTopicHeader: DiscussionTopicHeader, discussionTopic: DiscussionTopic, topLevelReplyPosted: Boolean = false) - fun populateDiscussionTopicHeader(discussionTopicHeader: DiscussionTopicHeader, forceNetwork: Boolean) - fun populateAsForbidden() - fun updateSubmissionDonuts(totalStudents: Int, gradedStudents: Int, needsGradingCount: Int, notSubmitted: Int) - fun updateDiscussionLiked(discussionEntry: DiscussionEntry) - fun updateDiscussionUnliked(discussionEntry: DiscussionEntry) - fun updateDiscussionsMarkedAsReadCompleted(markedAsReadIds: List) - fun updateDiscussionsMarkedAsUnreadCompleted(markedAsUnreadId: Long) - fun updateDiscussionAsDeleted(discussionEntry: DiscussionEntry) - fun updateDiscussionEntry(discussionEntry: DiscussionEntry) - fun showAnonymousDiscussionView() -} diff --git a/apps/teacher/src/main/java/com/instructure/teacher/viewinterface/DiscussionsReplyView.kt b/apps/teacher/src/main/java/com/instructure/teacher/viewinterface/DiscussionsReplyView.kt deleted file mode 100644 index 6ad48a29f2..0000000000 --- a/apps/teacher/src/main/java/com/instructure/teacher/viewinterface/DiscussionsReplyView.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (C) 2017 - present Instructure, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3 of the License. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ -package com.instructure.teacher.viewinterface - -import com.instructure.canvasapi2.models.DiscussionEntry -import com.instructure.teacher.interfaces.RceMediaUploadView -import instructure.androidblueprint.FragmentViewInterface - -interface DiscussionsReplyView : FragmentViewInterface, RceMediaUploadView { - fun messageSuccess(entry: DiscussionEntry) - fun messageFailure(reason: Int) -} diff --git a/apps/teacher/src/main/res/layout/fragment_discussions_details.xml b/apps/teacher/src/main/res/layout/fragment_discussions_details.xml deleted file mode 100644 index a223c4cad5..0000000000 --- a/apps/teacher/src/main/res/layout/fragment_discussions_details.xml +++ /dev/null @@ -1,532 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/teacher/src/main/res/layout/fragment_discussions_reply.xml b/apps/teacher/src/main/res/layout/fragment_discussions_reply.xml deleted file mode 100644 index a0a73a93ba..0000000000 --- a/apps/teacher/src/main/res/layout/fragment_discussions_reply.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - diff --git a/apps/teacher/src/test/java/com/instructure/teacher/features/discussion/routing/DiscussionRouteHelperTeacherRepositoryTest.kt b/apps/teacher/src/test/java/com/instructure/teacher/features/discussion/routing/DiscussionRouteHelperTeacherRepositoryTest.kt index a1609fd6c5..89cb7981e3 100644 --- a/apps/teacher/src/test/java/com/instructure/teacher/features/discussion/routing/DiscussionRouteHelperTeacherRepositoryTest.kt +++ b/apps/teacher/src/test/java/com/instructure/teacher/features/discussion/routing/DiscussionRouteHelperTeacherRepositoryTest.kt @@ -17,12 +17,10 @@ class DiscussionRouteHelperTeacherRepositoryTest { private val repository = DiscussionRouteHelperTeacherRepository(networkDataSource) @Test - fun `getEnabledFeaturesForCourse() calls networkDataSource`() = runTest { + fun `Always show discussion redesign`() = runTest { val expected = true - coEvery { networkDataSource.getEnabledFeaturesForCourse(any(), any()) } returns expected - - val result = repository.getEnabledFeaturesForCourse(mockk(), true) + val result = repository.shouldShowDiscussionRedesign() assertEquals(expected, result) } diff --git a/apps/teacher/src/test/java/com/instructure/teacher/unit/modules/progression/ModuleProgressionViewModelTest.kt b/apps/teacher/src/test/java/com/instructure/teacher/unit/modules/progression/ModuleProgressionViewModelTest.kt index c4857d2dba..4e4d00fe74 100644 --- a/apps/teacher/src/test/java/com/instructure/teacher/unit/modules/progression/ModuleProgressionViewModelTest.kt +++ b/apps/teacher/src/test/java/com/instructure/teacher/unit/modules/progression/ModuleProgressionViewModelTest.kt @@ -49,7 +49,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.setMain import org.junit.After -import org.junit.Assert import org.junit.Assert.* import org.junit.Before import org.junit.Rule @@ -147,7 +146,7 @@ class ModuleProgressionViewModelTest { listOf( ModuleItemViewData.Page("pageUrl"), ModuleItemViewData.Assignment(1L), - ModuleItemViewData.Discussion(true, 2L), + ModuleItemViewData.Discussion(2L), ModuleItemViewData.Quiz(3L), ModuleItemViewData.External("mockUri", "Title 1"), ModuleItemViewData.External("mockUri", "Title 2"), @@ -158,7 +157,7 @@ class ModuleProgressionViewModelTest { 0 ) - coEvery { discussionRouteHelperRepository.getEnabledFeaturesForCourse(any(), any()) } returns true + coEvery { discussionRouteHelperRepository.shouldShowDiscussionRedesign() } returns true coEvery { repository.getModulesWithItems(any()) } returns listOf( ModuleObject( diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CalendarInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CalendarInteractionTest.kt index 620126b140..96e8d3ec56 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CalendarInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CalendarInteractionTest.kt @@ -17,6 +17,7 @@ package com.instructure.canvas.espresso.common.interaction import com.instructure.canvas.espresso.CanvasComposeTest import com.instructure.canvas.espresso.StubLandscape +import com.instructure.canvas.espresso.StubTablet import com.instructure.canvas.espresso.common.pages.compose.CalendarEventDetailsPage import com.instructure.canvas.espresso.common.pages.compose.CalendarFilterPage import com.instructure.canvas.espresso.common.pages.compose.CalendarScreenPage @@ -345,6 +346,7 @@ abstract class CalendarInteractionTest : CanvasComposeTest() { } @Test + @StubTablet("Known issue, see MBL-17776") fun selectDiscussionOpensDiscussionDetails() { val data = initData() diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/DiscussionAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/DiscussionAPI.kt index 9b592b8532..9c32950b4e 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/DiscussionAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/DiscussionAPI.kt @@ -82,9 +82,6 @@ object DiscussionAPI { @GET("{contextType}/{contextId}/discussion_topics?override_assignment_dates=true&include[]=all_dates&include[]=overrides&include[]=sections") suspend fun getFirstPageDiscussionTopicHeaders(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Tag params: RestParams): DataResult> - @GET("{contextType}/{contextId}/discussion_topics/{topicId}?include[]=sections") - fun getDetailedDiscussion(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long): Call - @GET("{contextType}/{contextId}/discussion_topics/{topicId}?include[]=sections") suspend fun getDetailedDiscussion(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long, @Tag params: RestParams): DataResult @@ -94,18 +91,12 @@ object DiscussionAPI { @GET("{contextType}/{contextId}/discussion_topics/{topicId}/view") suspend fun getFullDiscussionTopic(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long, @Query("include_new_entries") includeNewEntries: Int, @Tag params: RestParams): DataResult - @POST("{contextType}/{contextId}/discussion_topics/{topicId}/entries/{entryId}/rating") - fun rateDiscussionEntry(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long, @Path("entryId") entryId: Long, @Query("rating") rating: Int): Call - @POST("{contextType}/{contextId}/discussion_topics/{topicId}/entries/{entryId}/rating") suspend fun rateDiscussionEntry(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long, @Path("entryId") entryId: Long, @Query("rating") rating: Int, @Tag params: RestParams): DataResult @PUT("{contextType}/{contextId}/discussion_topics/{topicId}/read") fun markDiscussionTopicRead(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long): Call - @PUT("{contextType}/{contextId}/discussion_topics/{topicId}/entries/{entryId}/read") - fun markDiscussionTopicEntryRead(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long, @Path("entryId") entryId: Long): Call - @PUT("{contextType}/{contextId}/discussion_topics/{topicId}/entries/{entryId}/read") suspend fun markDiscussionTopicEntryRead(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long, @Path("entryId") entryId: Long, @Tag params: RestParams): DataResult @@ -121,9 +112,6 @@ object DiscussionAPI { @DELETE("{contextType}/{contextId}/discussion_topics/{topicId}") fun deleteDiscussionTopic(@Path("contextType") contextType: String, @Path("contextId") courseId: Long, @Path("topicId") topicId: Long): Call - @DELETE("{contextType}/{contextId}/discussion_topics/{topicId}/entries/{entryId}") - fun deleteDiscussionEntry(@Path("contextType") contextType: String, @Path("contextId") courseId: Long, @Path("topicId") topicId: Long, @Path("entryId") entryId: Long): Call - @DELETE("{contextType}/{contextId}/discussion_topics/{topicId}/entries/{entryId}") suspend fun deleteDiscussionEntry(@Path("contextType") contextType: String, @Path("contextId") courseId: Long, @Path("topicId") topicId: Long, @Path("entryId") entryId: Long, @Tag params: RestParams): DataResult @@ -131,16 +119,16 @@ object DiscussionAPI { @POST("{contextType}/{contextId}/discussion_topics/{topicId}/entries/{entryId}/replies") fun postDiscussionReply(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long, @Path("entryId") entryId: Long, @Part("message") message: RequestBody): Call + @Multipart + @POST("{contextType}/{contextId}/discussion_topics/{topicId}/entries") + fun postDiscussionEntry(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long, @Part("message") message: RequestBody): Call + @Multipart @POST("{contextType}/{contextId}/discussion_topics/{topicId}/entries/{entryId}/replies") fun postDiscussionReplyWithAttachment(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long, @Path("entryId") entryId: Long, @Part("message") message: RequestBody, @Part attachment: MultipartBody.Part): Call - @Multipart - @POST("{contextType}/{contextId}/discussion_topics/{topicId}/entries") - fun postDiscussionEntry(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @Path("topicId") topicId: Long, @Part("message") message: RequestBody): Call - @Multipart @POST("{contextType}/{contextId}/discussion_topics/{topicId}/entries") fun postDiscussionEntryWithAttachment(@Path("contextType") contextType: String, @Path("contextId") contextId: Long, @@ -245,16 +233,6 @@ object DiscussionAPI { callback.addCall(adapter.build(DiscussionInterface::class.java, params).getNextPage(nextUrl)).enqueue(callback) } - fun getFullDiscussionTopic(adapter: RestBuilder, canvasContext: CanvasContext, topicId: Long, callback: StatusCallback, params: RestParams) { - val contextType = CanvasContext.getApiContext(canvasContext) - callback.addCall(adapter.build(DiscussionInterface::class.java, params).getFullDiscussionTopic(contextType, canvasContext.id, topicId, 1)).enqueue(callback) - } - - fun getDetailedDiscussion(adapter: RestBuilder, canvasContext: CanvasContext, topicId: Long, callback: StatusCallback, params: RestParams) { - val contextType = CanvasContext.getApiContext(canvasContext) - callback.addCall(adapter.build(DiscussionInterface::class.java, params).getDetailedDiscussion(contextType, canvasContext.id, topicId)).enqueue(callback) - } - fun replyToDiscussionEntry(adapter: RestBuilder, canvasContext: CanvasContext, topicId: Long, entryId: Long, message: String, callback: StatusCallback, params: RestParams) { val contextType = CanvasContext.getApiContext(canvasContext) @@ -295,26 +273,11 @@ object DiscussionAPI { callback.addCall(adapter.build(DiscussionInterface::class.java, params).postDiscussionEntryWithAttachment(contextType, canvasContext.id, topicId, messagePart, attachmentPart)).enqueue(callback) } - fun rateDiscussionEntry(adapter: RestBuilder, canvasContext: CanvasContext, topicId: Long, entryId: Long, rating: Int, callback: StatusCallback, params: RestParams) { - val contextType = CanvasContext.getApiContext(canvasContext) - callback.addCall(adapter.build(DiscussionInterface::class.java, params).rateDiscussionEntry(contextType, canvasContext.id, topicId, entryId, rating)).enqueue(callback) - } - fun markDiscussionTopicRead(adapter: RestBuilder, canvasContext: CanvasContext, topicId: Long, callback: StatusCallback, params: RestParams) { val contextType = CanvasContext.getApiContext(canvasContext) callback.addCall(adapter.build(DiscussionInterface::class.java, params).markDiscussionTopicRead(contextType, canvasContext.id, topicId)).enqueue(callback) } - fun markDiscussionTopicEntryRead(adapter: RestBuilder, canvasContext: CanvasContext, topicId: Long, entryId: Long, callback: StatusCallback, params: RestParams) { - val contextType = CanvasContext.getApiContext(canvasContext) - callback.addCall(adapter.build(DiscussionInterface::class.java, params).markDiscussionTopicEntryRead(contextType, canvasContext.id, topicId, entryId)).enqueue(callback) - } - - fun markDiscussionTopicEntryUnread(adapter: RestBuilder, canvasContext: CanvasContext, topicId: Long, entryId: Long, callback: StatusCallback, params: RestParams) { - val contextType = CanvasContext.getApiContext(canvasContext) - callback.addCall(adapter.build(DiscussionInterface::class.java, params).markDiscussionTopicEntryUnread(contextType, canvasContext.id, topicId, entryId)).enqueue(callback) - } - fun pinDiscussion(adapter: RestBuilder, canvasContext: CanvasContext, topicId: Long, callback: StatusCallback, params: RestParams) { callback.addCall(adapter.build(DiscussionInterface::class.java, params).pinDiscussion(CanvasContext.getApiContext(canvasContext), canvasContext.id, topicId, true, "")).enqueue(callback) } @@ -339,10 +302,6 @@ object DiscussionAPI { callback.addCall(adapter.build(DiscussionInterface::class.java, params).editDiscussionTopic(CanvasContext.getApiContext(canvasContext), canvasContext.id, topicId, body)).enqueue(callback) } - fun deleteDiscussionEntry(adapter: RestBuilder, canvasContext: CanvasContext, topicId: Long, entryId: Long, callback: StatusCallback, params: RestParams) { - callback.addCall(adapter.build(DiscussionInterface::class.java, params).deleteDiscussionEntry(CanvasContext.getApiContext(canvasContext), canvasContext.id, topicId, entryId)).enqueue(callback) - } - fun getDiscussionTopicHeader(adapter: RestBuilder, canvasContext: CanvasContext, topicId: Long, callback: StatusCallback, params: RestParams) { callback.addCall(adapter.build(DiscussionInterface::class.java, params).getDiscussionTopicHeader(CanvasContext.getApiContext(canvasContext), canvasContext.id, topicId)).enqueue(callback) } diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/DiscussionManager.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/DiscussionManager.kt index f6177d4c5e..0871cc2e83 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/DiscussionManager.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/managers/DiscussionManager.kt @@ -101,40 +101,6 @@ object DiscussionManager { DiscussionAPI.getFirstPageDiscussionTopicHeaders(canvasContext, adapter, depaginatedCallback, params) } - fun getFullDiscussionTopic( - canvasContext: CanvasContext, - topicId: Long, - forceNetwork: Boolean, - callback: StatusCallback - ) { - val adapter = RestBuilder(callback) - val params = RestParams(isForceReadFromNetwork = forceNetwork) - DiscussionAPI.getFullDiscussionTopic(adapter, canvasContext, topicId, callback, params) - } - - fun getDetailedDiscussion( - canvasContext: CanvasContext, - topicId: Long, - callback: StatusCallback, - forceNetwork: Boolean - ) { - val adapter = RestBuilder(callback) - val params = RestParams(isForceReadFromNetwork = forceNetwork) - DiscussionAPI.getDetailedDiscussion(adapter, canvasContext, topicId, callback, params) - } - - fun rateDiscussionEntry( - canvasContext: CanvasContext, - topicId: Long, - entryId: Long, - rating: Int, - callback: StatusCallback - ) { - val adapter = RestBuilder(callback) - val params = RestParams() - DiscussionAPI.rateDiscussionEntry(adapter, canvasContext, topicId, entryId, rating, callback, params) - } - fun markDiscussionTopicRead( canvasContext: CanvasContext, topicId: Long, @@ -145,28 +111,6 @@ object DiscussionManager { DiscussionAPI.markDiscussionTopicRead(adapter, canvasContext, topicId, callback, params) } - fun markDiscussionTopicEntryRead( - canvasContext: CanvasContext, - topicId: Long, - entryId: Long, - callback: StatusCallback - ) { - val adapter = RestBuilder(callback) - val params = RestParams() - DiscussionAPI.markDiscussionTopicEntryRead(adapter, canvasContext, topicId, entryId, callback, params) - } - - fun markDiscussionTopicEntryUnread( - canvasContext: CanvasContext, - topicId: Long, - entryId: Long, - callback: StatusCallback - ) { - val adapter = RestBuilder(callback) - val params = RestParams() - DiscussionAPI.markDiscussionTopicEntryUnread(adapter, canvasContext, topicId, entryId, callback, params) - } - fun replyToDiscussionEntry( canvasContext: CanvasContext, topicId: Long, @@ -302,17 +246,6 @@ object DiscussionManager { DiscussionAPI.deleteDiscussionTopicHeader(adapter, canvasContext, topicId, callback, params) } - fun deleteDiscussionEntry( - canvasContext: CanvasContext, - topicId: Long, - entryId: Long, - callback: StatusCallback - ) { - val adapter = RestBuilder(callback) - val params = RestParams() - DiscussionAPI.deleteDiscussionEntry(adapter, canvasContext, topicId, entryId, callback, params) - } - fun createDiscussion( canvasContext: CanvasContext, title: String, diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/di/DiscussionModule.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/di/DiscussionModule.kt index 9a91cc948d..f757f4a794 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/di/DiscussionModule.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/di/DiscussionModule.kt @@ -1,7 +1,6 @@ package com.instructure.pandautils.di import com.instructure.canvasapi2.apis.DiscussionAPI -import com.instructure.canvasapi2.apis.FeaturesAPI import com.instructure.canvasapi2.apis.GroupAPI import com.instructure.pandautils.features.discussion.router.DiscussionRouteHelper import com.instructure.pandautils.features.discussion.router.DiscussionRouteHelperLocalDataSource @@ -9,7 +8,6 @@ import com.instructure.pandautils.features.discussion.router.DiscussionRouteHelp import com.instructure.pandautils.features.discussion.router.DiscussionRouteHelperRepository import com.instructure.pandautils.room.offline.facade.DiscussionTopicHeaderFacade import com.instructure.pandautils.room.offline.facade.GroupFacade -import com.instructure.pandautils.utils.FeatureFlagProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -30,10 +28,8 @@ class DiscussionModule { fun provideDiscussionRouteHelperNetworkDataSource( discussionApi: DiscussionAPI.DiscussionInterface, groupApi: GroupAPI.GroupInterface, - featuresApi: FeaturesAPI.FeaturesInterface, - featureFlagProvider: FeatureFlagProvider ): DiscussionRouteHelperNetworkDataSource { - return DiscussionRouteHelperNetworkDataSource(discussionApi, groupApi, featuresApi, featureFlagProvider) + return DiscussionRouteHelperNetworkDataSource(discussionApi, groupApi) } @Provides diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelper.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelper.kt index 8284162236..8256b949d3 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelper.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelper.kt @@ -11,8 +11,8 @@ class DiscussionRouteHelper( private val discussionRouteHelperRepository: DiscussionRouteHelperRepository, ) { - suspend fun isDiscussionRedesignEnabled(canvasContext: CanvasContext): Boolean { - return discussionRouteHelperRepository.getEnabledFeaturesForCourse(canvasContext, false) + suspend fun shouldShowDiscussionRedesign(): Boolean { + return discussionRouteHelperRepository.shouldShowDiscussionRedesign() } suspend fun getDiscussionHeader( diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelperNetworkDataSource.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelperNetworkDataSource.kt index fc6460487a..0720df2ee5 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelperNetworkDataSource.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelperNetworkDataSource.kt @@ -19,40 +19,17 @@ package com.instructure.pandautils.features.discussion.router import com.instructure.canvasapi2.apis.DiscussionAPI -import com.instructure.canvasapi2.apis.FeaturesAPI import com.instructure.canvasapi2.apis.GroupAPI import com.instructure.canvasapi2.builders.RestParams import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.models.Group import com.instructure.canvasapi2.utils.depaginate -import com.instructure.pandautils.utils.FeatureFlagProvider -import com.instructure.pandautils.utils.isCourse -import com.instructure.pandautils.utils.isGroup class DiscussionRouteHelperNetworkDataSource( private val discussionApi: DiscussionAPI.DiscussionInterface, - private val groupApi: GroupAPI.GroupInterface, - private val featuresApi: FeaturesAPI.FeaturesInterface, - private val featureFlagProvider: FeatureFlagProvider + private val groupApi: GroupAPI.GroupInterface ) : DiscussionRouteHelperDataSource { - suspend fun getEnabledFeaturesForCourse(canvasContext: CanvasContext, forceNetwork: Boolean): Boolean { - val params = RestParams(isForceReadFromNetwork = forceNetwork) - return if (canvasContext.isCourse) { - val featureFlags = featuresApi.getEnabledFeaturesForCourse(canvasContext.id, params).dataOrNull - featureFlags?.contains("react_discussions_post") ?: false - } else if (canvasContext.isGroup) { - val group = canvasContext as Group - if (group.courseId == 0L) { - featureFlagProvider.getDiscussionRedesignFeatureFlag() - } else { - val featureFlags = featuresApi.getEnabledFeaturesForCourse(group.courseId, params).dataOrNull - featureFlags?.contains("react_discussions_post") ?: false - } - } else { - false - } - } override suspend fun getDiscussionTopicHeader( canvasContext: CanvasContext, diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelperRepository.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelperRepository.kt index de07ca8348..40d7aeb894 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelperRepository.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelperRepository.kt @@ -5,7 +5,7 @@ import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.models.Group interface DiscussionRouteHelperRepository { - suspend fun getEnabledFeaturesForCourse(canvasContext: CanvasContext, forceNetwork: Boolean): Boolean + suspend fun shouldShowDiscussionRedesign(): Boolean suspend fun getDiscussionTopicHeader(canvasContext: CanvasContext, discussionTopicHeaderId: Long, forceNetwork: Boolean): DiscussionTopicHeader? suspend fun getAllGroups(discussionTopicHeader: DiscussionTopicHeader, userId: Long, forceNetwork: Boolean): List } \ No newline at end of file diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouterViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouterViewModel.kt index fb5861937e..861c58b901 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouterViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/discussion/router/DiscussionRouterViewModel.kt @@ -32,7 +32,7 @@ class DiscussionRouterViewModel @Inject constructor( ) { viewModelScope.launch { try { - val discussionRedesignEnabled = discussionRouteHelper.isDiscussionRedesignEnabled(canvasContext) + val shouldShowDiscussionRedesign = discussionRouteHelper.shouldShowDiscussionRedesign() val header = discussionTopicHeader ?: discussionRouteHelper.getDiscussionHeader( canvasContext, discussionTopicHeaderId @@ -46,11 +46,11 @@ class DiscussionRouterViewModel @Inject constructor( it.first, it.second, groupDiscussionHeader, - discussionRedesignEnabled + shouldShowDiscussionRedesign ) - } ?: routeToDiscussion(canvasContext, header, discussionRedesignEnabled, isAnnouncement) + } ?: routeToDiscussion(canvasContext, header, shouldShowDiscussionRedesign, isAnnouncement) } else { - routeToDiscussion(canvasContext, header, discussionRedesignEnabled, isAnnouncement) + routeToDiscussion(canvasContext, header, shouldShowDiscussionRedesign, isAnnouncement) } } catch (e: Exception) { @@ -64,7 +64,7 @@ class DiscussionRouterViewModel @Inject constructor( group: Group, discussionTopicHeaderId: Long, discussionTopicHeader: DiscussionTopicHeader, - isRedesignEnabled: Boolean + shoudShowDiscussionRedesign: Boolean ) { _events.postValue( Event( @@ -72,7 +72,7 @@ class DiscussionRouterViewModel @Inject constructor( group, discussionTopicHeaderId, discussionTopicHeader, - isRedesignEnabled + shoudShowDiscussionRedesign ) ) ) @@ -81,14 +81,14 @@ class DiscussionRouterViewModel @Inject constructor( private fun routeToDiscussion( canvasContext: CanvasContext, header: DiscussionTopicHeader, - discussionRedesignEnabled: Boolean, + shouldShowDiscussionRedesign: Boolean, isAnnouncement: Boolean ) { _events.postValue( Event( DiscussionRouterAction.RouteToDiscussion( canvasContext, - discussionRedesignEnabled, + shouldShowDiscussionRedesign, header, isAnnouncement ) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/FeatureFlagProvider.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/FeatureFlagProvider.kt index f55e7a1b89..505d93fee1 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/FeatureFlagProvider.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/FeatureFlagProvider.kt @@ -43,10 +43,6 @@ class FeatureFlagProvider( } } - suspend fun getDiscussionRedesignFeatureFlag(): Boolean { - return checkEnvironmentFeatureFlag("react_discussions_post") - } - suspend fun fetchEnvironmentFeatureFlags() { val restParams = RestParams(isForceReadFromNetwork = true, shouldIgnoreToken = true) val featureFlags = featuresApi.getEnvironmentFeatureFlags(restParams).dataOrNull ?: return diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelperNetworkDataSourceTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelperNetworkDataSourceTest.kt index e2dabc19cf..d8ca21757c 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelperNetworkDataSourceTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/discussion/router/DiscussionRouteHelperNetworkDataSourceTest.kt @@ -19,17 +19,14 @@ package com.instructure.pandautils.features.discussion.router import com.instructure.canvasapi2.apis.DiscussionAPI -import com.instructure.canvasapi2.apis.FeaturesAPI import com.instructure.canvasapi2.apis.GroupAPI import com.instructure.canvasapi2.models.CanvasContext import com.instructure.canvasapi2.models.DiscussionTopicHeader import com.instructure.canvasapi2.models.Group import com.instructure.canvasapi2.models.GroupTopicChild import com.instructure.canvasapi2.utils.DataResult -import com.instructure.pandautils.utils.FeatureFlagProvider import io.mockk.coEvery import io.mockk.mockk -import junit.framework.TestCase import junit.framework.TestCase.assertEquals import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -39,47 +36,9 @@ import org.junit.Test class DiscussionRouteHelperNetworkDataSourceTest { private val discussionApi: DiscussionAPI.DiscussionInterface = mockk(relaxed = true) private val groupApi: GroupAPI.GroupInterface = mockk(relaxed = true) - private val featuresApi: FeaturesAPI.FeaturesInterface = mockk(relaxed = true) - private val featureFlagProvider: FeatureFlagProvider = mockk(relaxed = true) private val dataSource = - DiscussionRouteHelperNetworkDataSource(discussionApi, groupApi, featuresApi, featureFlagProvider) - - @Test - fun `getEnabledFeaturesForCourse returns api result if discussion redesign flag is true`() = runTest { - val canvasContext = CanvasContext.emptyCourseContext() - coEvery { featureFlagProvider.getDiscussionRedesignFeatureFlag() } returns true - coEvery { - featuresApi.getEnabledFeaturesForCourse( - any(), - any() - ) - } returns DataResult.Success(listOf("react_discussions_post")) - - val expected = true - - val result = dataSource.getEnabledFeaturesForCourse(canvasContext, true) - - assertEquals(expected, result) - } - - @Test - fun `getEnabledFeaturesForCourse returns api result if discussion redesign flag is false`() = runTest { - val canvasContext = CanvasContext.defaultCanvasContext() - coEvery { featureFlagProvider.getDiscussionRedesignFeatureFlag() } returns false - coEvery { - featuresApi.getEnabledFeaturesForCourse( - any(), - any() - ) - } returns DataResult.Success(listOf("react_discussions_post")) - - val expected = false - - val result = dataSource.getEnabledFeaturesForCourse(canvasContext, true) - - assertEquals(expected, result) - } + DiscussionRouteHelperNetworkDataSource(discussionApi, groupApi) @Test fun `getDiscussionTopicHeader returns correct data`() = runTest { diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/discussion/router/DiscussionRouterViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/discussion/router/DiscussionRouterViewModelTest.kt index 3f5c6d70f7..2eca719935 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/discussion/router/DiscussionRouterViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/discussion/router/DiscussionRouterViewModelTest.kt @@ -46,7 +46,7 @@ class DiscussionRouterViewModelTest { mockkStatic("kotlinx.coroutines.AwaitKt") - coEvery { discussionRouteHelper.isDiscussionRedesignEnabled(any()) } returns true + coEvery { discussionRouteHelper.shouldShowDiscussionRedesign() } returns true viewModel = DiscussionRouterViewModel(discussionRouteHelper, resources) @@ -58,7 +58,7 @@ class DiscussionRouterViewModelTest { val course = Course() val discussionTopicHeader = DiscussionTopicHeader(1L) - coEvery { discussionRouteHelper.isDiscussionRedesignEnabled(any()) } returns false + coEvery { discussionRouteHelper.shouldShowDiscussionRedesign() } returns false viewModel.events.observe(lifecycleOwner) {} @@ -75,7 +75,7 @@ class DiscussionRouterViewModelTest { val course = Course() val discussionTopicHeader = DiscussionTopicHeader(1L) - coEvery { discussionRouteHelper.isDiscussionRedesignEnabled(any()) } returns true + coEvery { discussionRouteHelper.shouldShowDiscussionRedesign() } returns true viewModel.events.observe(lifecycleOwner) {} @@ -93,7 +93,7 @@ class DiscussionRouterViewModelTest { val discussionTopicHeader = DiscussionTopicHeader(1L, groupTopicChildren = listOf(GroupTopicChild(2L, 1L))) val groupDiscussionTopicHeader = DiscussionTopicHeader(2L) - coEvery { discussionRouteHelper.isDiscussionRedesignEnabled(any()) } returns false + coEvery { discussionRouteHelper.shouldShowDiscussionRedesign() } returns false coEvery { discussionRouteHelper.getDiscussionGroup(discussionTopicHeader) } returns Pair(group, 2L) coEvery { discussionRouteHelper.getDiscussionHeader(any(), any()) } returns groupDiscussionTopicHeader @@ -113,7 +113,7 @@ class DiscussionRouterViewModelTest { val discussionTopicHeader = DiscussionTopicHeader(1L, groupTopicChildren = listOf(GroupTopicChild(2L, 1L))) val groupDiscussionTopicHeader = DiscussionTopicHeader(2L) - coEvery { discussionRouteHelper.isDiscussionRedesignEnabled(any()) } returns true + coEvery { discussionRouteHelper.shouldShowDiscussionRedesign() } returns true coEvery { discussionRouteHelper.getDiscussionGroup(discussionTopicHeader) } returns Pair(group, 2L) coEvery { discussionRouteHelper.getDiscussionHeader(any(), any()) } returns groupDiscussionTopicHeader From 0735ad03ad680c9390559fcdb1a61b0f6f2ccc51 Mon Sep 17 00:00:00 2001 From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com> Date: Fri, 26 Jul 2024 16:08:52 +0200 Subject: [PATCH 16/50] [MBL-17741][Student] Embedded Kaltura playlist video's do not show on Pages. #2506 refs: MBL-17741 affects: Student release note: Fixed an issue where embedded Kaltura videos wouldn't show on pages. --- .../modules/progression/LockedModuleItemFragment.kt | 1 - .../student/fragment/InternalWebviewFragment.kt | 8 -------- .../teacher/fragments/InternalWebViewFragment.kt | 8 -------- .../pandautils/fragments/HtmlContentFragment.kt | 1 - 4 files changed, 18 deletions(-) diff --git a/apps/student/src/main/java/com/instructure/student/features/modules/progression/LockedModuleItemFragment.kt b/apps/student/src/main/java/com/instructure/student/features/modules/progression/LockedModuleItemFragment.kt index d708d01bd6..806f82655c 100644 --- a/apps/student/src/main/java/com/instructure/student/features/modules/progression/LockedModuleItemFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/modules/progression/LockedModuleItemFragment.kt @@ -75,7 +75,6 @@ class LockedModuleItemFragment : ParentFragment() { canvasWebView.settings.loadWithOverviewMode = true canvasWebView.settings.displayZoomControls = false canvasWebView.settings.setSupportZoom(true) - canvasWebView.settings.userAgentString = ApiPrefs.userAgent canvasWebView.addVideoClient(requireActivity()) canvasWebView.setInitialScale(100) diff --git a/apps/student/src/main/java/com/instructure/student/fragment/InternalWebviewFragment.kt b/apps/student/src/main/java/com/instructure/student/fragment/InternalWebviewFragment.kt index 0675d6fa6d..6ed6c3cff0 100644 --- a/apps/student/src/main/java/com/instructure/student/fragment/InternalWebviewFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/fragment/InternalWebviewFragment.kt @@ -77,9 +77,6 @@ open class InternalWebviewFragment : ParentFragment() { var hideToolbar: Boolean by BooleanArg(key = Const.HIDDEN_TOOLBAR) - // Used for external urls that reject the candroid user agent string - var originalUserAgentString: String = "" - /* * Our router has some catch-all routes which open the UnsupportedFeatureFragment for urls that match the user's * domain but don't match any other internal routes. In some cases, such as viewing an HTML file preview, we need to @@ -112,8 +109,6 @@ open class InternalWebviewFragment : ParentFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) = with(binding) { super.onViewCreated(view, savedInstanceState) canvasWebViewWrapper.webView.settings.loadWithOverviewMode = true - originalUserAgentString = canvasWebViewWrapper.webView.settings.userAgentString - canvasWebViewWrapper.webView.settings.userAgentString = ApiPrefs.userAgent canvasWebViewWrapper.webView.setInitialScale(100) webViewLoading.setVisible(true) @@ -395,9 +390,6 @@ open class InternalWebviewFragment : ParentFragment() { } catch (e: StatusCallbackError) { e.printStackTrace() } - } else { - // External URL, use the non-Canvas specific user agent string - binding.canvasWebViewWrapper.webView.settings.userAgentString = originalUserAgentString } if (getIsUnsupportedFeature()) { diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/InternalWebViewFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/InternalWebViewFragment.kt index 1c9aa25cce..e535ed0323 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/InternalWebViewFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/InternalWebViewFragment.kt @@ -75,9 +75,6 @@ open class InternalWebViewFragment : BaseFragment() { override fun layoutResId() = R.layout.fragment_internal_webview - // Used for external urls that reject the candroid user agent string - var originalUserAgentString: String = "" - override fun onPause() { super.onPause() binding.canvasWebView.onPause() @@ -139,8 +136,6 @@ open class InternalWebViewFragment : BaseFragment() { canvasWebView.settings.loadWithOverviewMode = true canvasWebView.settings.displayZoomControls = false canvasWebView.settings.setSupportZoom(true) - originalUserAgentString = canvasWebView.settings.userAgentString - canvasWebView.settings.userAgentString = ApiPrefs.userAgent canvasWebView.addVideoClient(requireActivity()) canvasWebView.setInitialScale(100) @@ -229,9 +224,6 @@ open class InternalWebViewFragment : BaseFragment() { url = awaitApi { OAuthManager.getAuthenticatedSession(url, it) }.sessionUrl } catch (e: StatusCallbackError) { } - } else { - // External URL, use the non-Canvas specific user agent string - canvasWebView.settings.userAgentString = originalUserAgentString } canvasWebView.loadUrl(url, getReferer()) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/fragments/HtmlContentFragment.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/fragments/HtmlContentFragment.kt index f8b2b314da..088699f346 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/fragments/HtmlContentFragment.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/fragments/HtmlContentFragment.kt @@ -68,7 +68,6 @@ class HtmlContentFragment : Fragment() { canvasWebView.settings.loadWithOverviewMode = true canvasWebView.settings.displayZoomControls = false canvasWebView.settings.setSupportZoom(true) - canvasWebView.settings.userAgentString = ApiPrefs.userAgent canvasWebView.addVideoClient(requireActivity()) canvasWebView.setInitialScale(100) From 8e09574ddd256d71ad0348c5d8676d12427e85dc Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Mon, 29 Jul 2024 10:24:35 +0200 Subject: [PATCH 17/50] Attempt to stabilize letter grade related tests on nightly by repeating the refresh and assert flow after the api call with an increasing delay up to maximum 4 sec. (#2509) --- .../student/ui/e2e/AssignmentsE2ETest.kt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt index c61b83cc1e..aa4ad7278a 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/AssignmentsE2ETest.kt @@ -39,6 +39,7 @@ import com.instructure.dataseeding.util.ago import com.instructure.dataseeding.util.days import com.instructure.dataseeding.util.fromNow import com.instructure.dataseeding.util.iso8601 +import com.instructure.espresso.retryWithIncreasingDelay import com.instructure.student.R import com.instructure.student.ui.pages.AssignmentListPage import com.instructure.student.ui.utils.StudentTest @@ -785,8 +786,10 @@ class AssignmentsE2ETest: StudentTest() { CoursesApi.updateCourseSettings(course.id, restrictQuantitativeDataMap) Log.d(STEP_TAG, "Refresh the Dashboard page. Assert that the course grade is B-, as it is converted to letter grade because of the restriction.") - dashboardPage.refresh() - dashboardPage.assertCourseGrade(course.name, "B-") + retryWithIncreasingDelay(times = 10, maxDelay = 4000) { + dashboardPage.refresh() + dashboardPage.assertCourseGrade(course.name, "B-") + } Log.d(PREPARATION_TAG,"Seeding 'Text Entry' assignment for ${course.name} course.") val percentageAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.PERCENT, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY)) @@ -937,8 +940,10 @@ class AssignmentsE2ETest: StudentTest() { CoursesApi.updateCourseSettings(course.id, restrictQuantitativeDataMap) Log.d(STEP_TAG, "Refresh the Dashboard page. Assert that the course grade is B-, as it is converted to letter grade because of the restriction.") - dashboardPage.refresh() - dashboardPage.assertCourseGrade(course.name, "B-") + retryWithIncreasingDelay(times = 10, maxDelay = 4000) { + dashboardPage.refresh() + dashboardPage.assertCourseGrade(course.name, "B-") + } Log.d(PREPARATION_TAG, "Seeding 'Text Entry' assignment for '${course.name}' course.") val percentageAssignment = AssignmentsApi.createAssignment(course.id, teacher.token, gradingType = GradingType.PERCENT, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601, submissionTypes = listOf(SubmissionType.ONLINE_TEXT_ENTRY)) From 6dc0e1d2b6a909776a4a6b465f272017db762811 Mon Sep 17 00:00:00 2001 From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com> Date: Mon, 29 Jul 2024 10:55:33 +0200 Subject: [PATCH 18/50] [MBL-17619][Parent] Manage Students refs: MBL-17619 affects: Parent release note: none * Manage students * Accessibility changes * Manage students unit tests * Manage students ui tests * Added student pronouns * spacing fix * test sorting --- .../ui/compose/ManageStudentsScreenTest.kt | 155 +++++++++ .../ManageStudentsInteractionTest.kt | 105 +++++++ .../parentapp/ui/pages/DashboardPage.kt | 4 + .../parentapp/ui/pages/ManageStudentsPage.kt | 62 ++++ .../parentapp/di/ManageStudentsModule.kt | 40 +++ .../managestudents/ManageStudentViewModel.kt | 198 ++++++++++++ .../managestudents/ManageStudentsFragment.kt | 32 +- .../ManageStudentsRepository.kt | 47 +++ .../managestudents/ManageStudentsScreen.kt | 296 ++++++++++++++++++ .../managestudents/ManageStudentsUiState.kt | 66 ++++ .../StudentColorPickerDialog.kt | 199 ++++++++++++ .../ManageStudentsRepositoryTest.kt | 114 +++++++ .../ManageStudentsViewModelTest.kt | 269 ++++++++++++++++ buildSrc/src/main/java/GlobalDependencies.kt | 1 + .../instructure/canvasapi2/apis/UserAPI.kt | 7 + .../canvasapi2/models/ColorChangeResponse.kt | 27 ++ .../src/main/res/values-night/colors.xml | 4 - libs/pandares/src/main/res/values/colors.xml | 10 +- libs/pandares/src/main/res/values/strings.xml | 11 + libs/pandautils/build.gradle | 1 + .../compose/composables/EmptyContent.kt | 24 +- .../compose/composables/UserAvatar.kt | 75 +++++ .../pandautils/utils/ColorKeeper.kt | 24 +- .../pandautils/utils/ProfileUtils.kt | 4 +- 24 files changed, 1741 insertions(+), 34 deletions(-) create mode 100644 apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/ManageStudentsScreenTest.kt create mode 100644 apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ManageStudentsInteractionTest.kt create mode 100644 apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/ManageStudentsPage.kt create mode 100644 apps/parent/src/main/java/com/instructure/parentapp/di/ManageStudentsModule.kt create mode 100644 apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentViewModel.kt create mode 100644 apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentsRepository.kt create mode 100644 apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentsScreen.kt create mode 100644 apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentsUiState.kt create mode 100644 apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/StudentColorPickerDialog.kt create mode 100644 apps/parent/src/test/java/com/instructure/parentapp/features/managestudents/ManageStudentsRepositoryTest.kt create mode 100644 apps/parent/src/test/java/com/instructure/parentapp/features/managestudents/ManageStudentsViewModelTest.kt create mode 100644 libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/ColorChangeResponse.kt create mode 100644 libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/UserAvatar.kt diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/ManageStudentsScreenTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/ManageStudentsScreenTest.kt new file mode 100644 index 0000000000..bb70b4e921 --- /dev/null +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/compose/ManageStudentsScreenTest.kt @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.parentapp.ui.compose + +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasAnyChild +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.instructure.pandares.R +import com.instructure.pandautils.utils.ThemedColor +import com.instructure.parentapp.features.managestudents.ColorPickerDialogUiState +import com.instructure.parentapp.features.managestudents.ManageStudentsScreen +import com.instructure.parentapp.features.managestudents.ManageStudentsUiState +import com.instructure.parentapp.features.managestudents.StudentItemUiState +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + + +@RunWith(AndroidJUnit4::class) +class ManageStudentsScreenTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun assertEmptyContent() { + composeTestRule.setContent { + ManageStudentsScreen( + uiState = ManageStudentsUiState( + isLoading = false, + studentListItems = emptyList() + ), + actionHandler = {}, + navigationActionClick = {} + ) + } + + composeTestRule.onNodeWithText("You are not observing any students.") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Retry") + .assertIsDisplayed() + .assertHasClickAction() + composeTestRule.onNodeWithTag(R.drawable.panda_manage_students.toString()) + .assertIsDisplayed() + } + + @Test + fun assertErrorContent() { + composeTestRule.setContent { + ManageStudentsScreen( + uiState = ManageStudentsUiState( + isLoadError = true, + studentListItems = emptyList() + ), + actionHandler = {}, + navigationActionClick = {} + ) + } + + composeTestRule.onNodeWithText("There was an error loading your students.") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Retry") + .assertIsDisplayed() + .assertHasClickAction() + } + + @Test + fun assertStudentListContent() { + composeTestRule.setContent { + ManageStudentsScreen( + uiState = ManageStudentsUiState( + studentListItems = listOf( + StudentItemUiState( + studentId = 1, + studentName = "John Doe", + studentPronouns = "He/Him", + studentColor = ThemedColor(R.color.studentGreen) + ), + StudentItemUiState( + studentId = 2, + studentName = "Jane Doe", + studentColor = ThemedColor(R.color.studentPink) + ) + ) + ), + actionHandler = {}, + navigationActionClick = {} + ) + } + + fun studentItemMatcher(name: String) = hasTestTag("studentListItem") and hasAnyChild(hasText(name)) + composeTestRule.onNode(studentItemMatcher("John Doe (He/Him)"), true) + .assertIsDisplayed() + composeTestRule.onNode(studentItemMatcher("Jane Doe"), true) + .assertIsDisplayed() + } + + @Test + fun assertColorPickerDialogError() { + composeTestRule.setContent { + ManageStudentsScreen( + uiState = ManageStudentsUiState( + studentListItems = listOf( + StudentItemUiState( + studentId = 1, + studentName = "John Doe", + studentColor = ThemedColor(R.color.studentGreen) + ) + ), + colorPickerDialogUiState = ColorPickerDialogUiState( + showColorPickerDialog = true, + studentId = 1, + initialUserColor = null, + userColors = emptyList(), + isSavingColorError = true + ) + ), + actionHandler = {}, + navigationActionClick = {} + ) + } + + composeTestRule.onNodeWithText("Select Student Color") + .assertIsDisplayed() + composeTestRule.onNodeWithText("An error occurred while saving your selection. Please try again.") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Cancel") + .assertIsDisplayed() + .assertHasClickAction() + composeTestRule.onNodeWithText("OK") + .assertIsDisplayed() + .assertHasClickAction() + } +} diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ManageStudentsInteractionTest.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ManageStudentsInteractionTest.kt new file mode 100644 index 0000000000..9e5b444aec --- /dev/null +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/interaction/ManageStudentsInteractionTest.kt @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.parentapp.ui.interaction + +import androidx.compose.ui.platform.ComposeView +import androidx.test.espresso.matcher.ViewMatchers +import com.google.android.apps.common.testing.accessibility.framework.AccessibilityCheckResultUtils +import com.google.android.apps.common.testing.accessibility.framework.checks.SpeakableTextPresentCheck +import com.instructure.canvas.espresso.mockCanvas.MockCanvas +import com.instructure.canvas.espresso.mockCanvas.init +import com.instructure.parentapp.ui.pages.ManageStudentsPage +import com.instructure.parentapp.utils.ParentComposeTest +import com.instructure.parentapp.utils.tokenLogin +import dagger.hilt.android.testing.HiltAndroidTest +import org.hamcrest.Matchers +import org.junit.Test + + +@HiltAndroidTest +class ManageStudentsInteractionTest : ParentComposeTest() { + + private val manageStudentsPage = ManageStudentsPage(composeTestRule) + + @Test + fun testStudentsDisplayed() { + val data = initData() + + goToManageStudents(data) + + composeTestRule.waitForIdle() + data.students.forEach { + manageStudentsPage.assertStudentItemDisplayed(it) + } + } + + @Test + fun testStudentTapped() { + val data = initData() + + goToManageStudents(data) + + composeTestRule.waitForIdle() + manageStudentsPage.tapStudent(data.students.first().shortName!!) + // TODO Assert alert settings when implemented + } + + @Test + fun testColorPickerDialog() { + val data = initData() + + goToManageStudents(data) + + composeTestRule.waitForIdle() + manageStudentsPage.tapStudentColor(data.students.first().shortName!!) + manageStudentsPage.assertColorPickerDialogDisplayed() + } + + private fun initData(): MockCanvas { + return MockCanvas.init( + parentCount = 1, + studentCount = 3, + courseCount = 1 + ) + } + + private fun goToManageStudents(data: MockCanvas) { + val parent = data.parents.first() + val token = data.tokenFor(parent)!! + tokenLogin(data.domain, token, parent) + dashboardPage.openNavigationDrawer() + dashboardPage.tapManageStudents() + } + + override fun enableAndConfigureAccessibilityChecks() { + extraAccessibilitySupressions = Matchers.allOf( + AccessibilityCheckResultUtils.matchesCheck( + SpeakableTextPresentCheck::class.java + ), + AccessibilityCheckResultUtils.matchesViews( + ViewMatchers.withParent( + ViewMatchers.withClassName( + Matchers.equalTo(ComposeView::class.java.name) + ) + ) + ) + ) + + super.enableAndConfigureAccessibilityChecks() + } +} \ No newline at end of file diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/DashboardPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/DashboardPage.kt index d627828de2..34013c15e5 100644 --- a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/DashboardPage.kt +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/DashboardPage.kt @@ -84,4 +84,8 @@ class DashboardPage : BasePage(R.id.drawer_layout) { fun clickAlerts() { alertsItem.click() } + + fun tapManageStudents() { + onViewWithText(R.string.screenTitleManageStudents).click() + } } diff --git a/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/ManageStudentsPage.kt b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/ManageStudentsPage.kt new file mode 100644 index 0000000000..a1b0eb904a --- /dev/null +++ b/apps/parent/src/androidTest/java/com/instructure/parentapp/ui/pages/ManageStudentsPage.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.parentapp.ui.pages + +import androidx.compose.ui.test.assertHasClickAction +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasAnyChild +import androidx.compose.ui.test.hasAnySibling +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import com.instructure.canvasapi2.models.User + + +class ManageStudentsPage(private val composeTestRule: ComposeTestRule) { + + fun assertStudentItemDisplayed(user: User) { + composeTestRule.onNodeWithText(user.shortName.orEmpty()) + .assertIsDisplayed() + composeTestRule.onNode(hasTestTag("studentListItem") and hasAnyChild(hasText(user.shortName.orEmpty())), true) + .assertIsDisplayed() + .assertHasClickAction() + } + + fun tapStudent(name: String) { + composeTestRule.onNodeWithText(name) + .assertIsDisplayed() + .performClick() + } + + fun tapStudentColor(name: String) { + composeTestRule.onNode(hasTestTag("studentColor") and hasAnySibling(hasText(name)), true) + .assertIsDisplayed() + .performClick() + } + + fun assertColorPickerDialogDisplayed() { + composeTestRule.onNodeWithText("Select Student Color") + .assertIsDisplayed() + composeTestRule.onNodeWithText("Cancel") + .assertIsDisplayed() + composeTestRule.onNodeWithText("OK") + .assertIsDisplayed() + } +} diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/ManageStudentsModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/ManageStudentsModule.kt new file mode 100644 index 0000000000..8b6ccebd9e --- /dev/null +++ b/apps/parent/src/main/java/com/instructure/parentapp/di/ManageStudentsModule.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.parentapp.di + +import com.instructure.canvasapi2.apis.EnrollmentAPI +import com.instructure.canvasapi2.apis.UserAPI +import com.instructure.parentapp.features.managestudents.ManageStudentsRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent + + +@Module +@InstallIn(ViewModelComponent::class) +class ManageStudentsModule { + + @Provides + fun provideManageStudentsRepository( + enrollmentsApi: EnrollmentAPI.EnrollmentInterface, + userApi: UserAPI.UsersInterface + ): ManageStudentsRepository { + return ManageStudentsRepository(enrollmentsApi, userApi) + } +} diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentViewModel.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentViewModel.kt new file mode 100644 index 0000000000..3bf6ac449a --- /dev/null +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentViewModel.kt @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.parentapp.features.managestudents + +import android.content.Context +import androidx.core.content.ContextCompat +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.instructure.canvasapi2.utils.weave.catch +import com.instructure.canvasapi2.utils.weave.tryLaunch +import com.instructure.pandautils.utils.ColorKeeper +import com.instructure.pandautils.utils.createThemedColor +import com.instructure.pandautils.utils.orDefault +import com.instructure.parentapp.R +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + + +@HiltViewModel +class ManageStudentViewModel @Inject constructor( + @ApplicationContext private val context: Context, + private val colorKeeper: ColorKeeper, + private val repository: ManageStudentsRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(ManageStudentsUiState()) + val uiState = _uiState.asStateFlow() + + private val _events = Channel() + val events = _events.receiveAsFlow() + + init { + loadStudents() + } + + private val userColorContentDescriptionMap = mapOf( + R.color.studentBlue to R.string.studentColorContentDescriptionBlue, + R.color.studentPurple to R.string.studentColorContentDescriptionPurple, + R.color.studentPink to R.string.studentColorContentDescriptionPink, + R.color.studentRed to R.string.studentColorContentDescriptionRed, + R.color.studentOrange to R.string.studentColorContentDescriptionOrange, + R.color.studentGreen to R.string.studentColorContentDescriptionGreen, + ) + + private val userColors by lazy { + colorKeeper.userColors.map { + UserColor( + colorRes = it, + color = createThemedColor(context.getColor(it)), + contentDescriptionRes = userColorContentDescriptionMap[it].orDefault() + ) + } + } + + private fun loadStudents(forceRefresh: Boolean = false) { + viewModelScope.tryLaunch { + _uiState.update { + it.copy( + isLoading = true, + isLoadError = false + ) + } + + val students = repository.getStudents(forceRefresh) + + _uiState.update { state -> + state.copy( + isLoading = false, + studentListItems = students.map { + StudentItemUiState( + studentId = it.id, + avatarUrl = it.avatarUrl, + studentName = it.shortName ?: it.name, + studentPronouns = it.pronouns, + studentColor = colorKeeper.getOrGenerateUserColor(it) + ) + } + ) + } + } catch { + _uiState.update { + it.copy( + isLoading = false, + isLoadError = true + ) + } + } + } + + private fun saveStudentColor(studentId: Long, selected: UserColor) { + viewModelScope.tryLaunch { + val contextId = "user_$studentId" + val color = ContextCompat.getColor(context, selected.colorRes) + + _uiState.update { + it.copy( + colorPickerDialogUiState = it.colorPickerDialogUiState.copy( + isSavingColor = true + ) + ) + } + + val result = repository.saveStudentColor(contextId, getHexString(color)) + if (result != null) { + colorKeeper.addToCache(contextId, color) + _uiState.update { + it.copy( + colorPickerDialogUiState = ColorPickerDialogUiState(), + studentListItems = it.studentListItems.map { studentItem -> + if (studentItem.studentId == studentId) { + studentItem.copy(studentColor = selected.color) + } else { + studentItem + } + } + ) + } + } else { + showSavingError() + } + } catch { + showSavingError() + } + } + + private fun getHexString(color: Int): String { + var hexColor = Integer.toHexString(color) + hexColor = hexColor.substring(hexColor.length - 6) + if (hexColor.contains("#")) { + hexColor = hexColor.replace("#".toRegex(), "") + } + return hexColor + } + + private fun showSavingError() { + _uiState.update { + it.copy( + colorPickerDialogUiState = it.colorPickerDialogUiState.copy( + isSavingColor = false, + isSavingColorError = true + ) + ) + } + } + + fun handleAction(action: ManageStudentsAction) { + when (action) { + is ManageStudentsAction.StudentTapped -> { + viewModelScope.launch { + _events.send(ManageStudentsViewModelAction.NavigateToAlertSettings(action.studentId)) + } + } + + is ManageStudentsAction.Refresh -> loadStudents(true) + is ManageStudentsAction.AddStudent -> {} //TODO: Add student flow + is ManageStudentsAction.ShowColorPickerDialog -> _uiState.update { + it.copy( + colorPickerDialogUiState = it.colorPickerDialogUiState.copy( + showColorPickerDialog = true, + studentId = action.studentId, + initialUserColor = userColors.find { userColor -> + userColor.color == action.studentColor + }, + userColors = userColors + ) + ) + } + + is ManageStudentsAction.HideColorPickerDialog -> _uiState.update { + it.copy(colorPickerDialogUiState = ColorPickerDialogUiState()) + } + + is ManageStudentsAction.StudentColorChanged -> saveStudentColor(action.studentId, action.userColor) + } + } +} diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentsFragment.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentsFragment.kt index 08a5d109cd..0777715802 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentsFragment.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentsFragment.kt @@ -21,23 +21,51 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.compose.material.Text +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import com.instructure.pandautils.utils.ThemePrefs +import com.instructure.pandautils.utils.ViewStyler +import com.instructure.pandautils.utils.collectOneOffEvents import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class ManageStudentsFragment : Fragment() { + private val viewModel: ManageStudentViewModel by viewModels() + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { + ViewStyler.setStatusBarDark(requireActivity(), ThemePrefs.primaryColor) + + lifecycleScope.collectOneOffEvents(viewModel.events, ::handleAction) + return ComposeView(requireActivity()).apply { setContent { - Text(text = "Manage Students") + val uiState by viewModel.uiState.collectAsState() + ManageStudentsScreen( + uiState, + viewModel::handleAction, + navigationActionClick = { + findNavController().popBackStack() + } + ) + } + } + } + + private fun handleAction(action: ManageStudentsViewModelAction) { + when (action) { + is ManageStudentsViewModelAction.NavigateToAlertSettings -> { + //TODO: Navigate to alert settings } } } diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentsRepository.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentsRepository.kt new file mode 100644 index 0000000000..f36fdb3495 --- /dev/null +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentsRepository.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.parentapp.features.managestudents + +import com.instructure.canvasapi2.apis.EnrollmentAPI +import com.instructure.canvasapi2.apis.UserAPI +import com.instructure.canvasapi2.builders.RestParams +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.depaginate + + +class ManageStudentsRepository( + private val enrollmentApi: EnrollmentAPI.EnrollmentInterface, + private val userApi: UserAPI.UsersInterface +) { + + suspend fun getStudents(forceRefresh: Boolean): List { + val params = RestParams(usePerPageQueryParam = true, isForceReadFromNetwork = forceRefresh) + return enrollmentApi.firstPageObserveeEnrollmentsParent(params).depaginate { + enrollmentApi.getNextPage(it, params) + }.dataOrNull + .orEmpty() + .mapNotNull { it.observedUser } + .distinct() + .sortedBy { it.sortableName } + } + + suspend fun saveStudentColor(contextId: String, color: String): String? { + val params = RestParams() + return userApi.setColor(contextId, color, params).dataOrNull?.hexCode + } +} diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentsScreen.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentsScreen.kt new file mode 100644 index 0000000000..d7fa920977 --- /dev/null +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentsScreen.kt @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.parentapp.features.managestudents + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.FloatingActionButton +import androidx.compose.material.Icon +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.instructure.canvasapi2.utils.ContextKeeper +import com.instructure.pandautils.R +import com.instructure.pandautils.compose.CanvasTheme +import com.instructure.pandautils.compose.composables.CanvasThemedAppBar +import com.instructure.pandautils.compose.composables.EmptyContent +import com.instructure.pandautils.compose.composables.ErrorContent +import com.instructure.pandautils.compose.composables.UserAvatar +import com.instructure.pandautils.utils.ThemePrefs + + +@Composable +internal fun ManageStudentsScreen( + uiState: ManageStudentsUiState, + actionHandler: (ManageStudentsAction) -> Unit, + navigationActionClick: () -> Unit, + modifier: Modifier = Modifier +) { + CanvasTheme { + Scaffold( + backgroundColor = colorResource(id = R.color.backgroundLightest), + topBar = { + CanvasThemedAppBar( + title = stringResource(id = R.string.screenTitleManageStudents), + navigationActionClick = { + navigationActionClick() + } + ) + }, + content = { padding -> + if (uiState.isLoadError) { + ErrorContent( + errorMessage = stringResource(id = R.string.errorLoadingStudents), + retryClick = { + actionHandler(ManageStudentsAction.Refresh) + }, + modifier = Modifier.fillMaxSize() + ) + } else if (uiState.studentListItems.isEmpty() && !uiState.isLoading) { + EmptyContent( + emptyMessage = stringResource(id = R.string.noStudentsErrorDescription), + imageRes = R.drawable.panda_manage_students, + buttonText = stringResource(id = R.string.retry), + buttonClick = { + actionHandler(ManageStudentsAction.Refresh) + }, + modifier = Modifier.fillMaxSize() + ) + } else { + StudentListContent( + uiState = uiState, + actionHandler = actionHandler, + modifier = Modifier + .padding(padding) + .fillMaxSize() + ) + } + }, + floatingActionButton = { + FloatingActionButton( + backgroundColor = Color(ThemePrefs.buttonColor), + onClick = { + actionHandler(ManageStudentsAction.AddStudent) + } + ) { + Icon( + painter = painterResource(id = R.drawable.ic_add), + tint = Color(ThemePrefs.buttonTextColor), + contentDescription = stringResource(id = R.string.addNewStudent) + ) + } + }, + modifier = modifier + ) + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun StudentListContent( + uiState: ManageStudentsUiState, + actionHandler: (ManageStudentsAction) -> Unit, + modifier: Modifier = Modifier +) { + val pullRefreshState = rememberPullRefreshState( + refreshing = uiState.isLoading, + onRefresh = { + actionHandler(ManageStudentsAction.Refresh) + } + ) + + val dialogUiState = uiState.colorPickerDialogUiState + if (dialogUiState.showColorPickerDialog) { + StudentColorPickerDialog( + initialUserColor = dialogUiState.initialUserColor, + userColors = dialogUiState.userColors, + saving = dialogUiState.isSavingColor, + error = dialogUiState.isSavingColorError, + onDismiss = { + actionHandler(ManageStudentsAction.HideColorPickerDialog) + }, + onColorSelected = { + actionHandler(ManageStudentsAction.StudentColorChanged(dialogUiState.studentId, it)) + } + ) + } + + Box( + modifier = modifier.pullRefresh(pullRefreshState) + ) { + LazyColumn( + modifier = Modifier.fillMaxSize() + ) { + items(uiState.studentListItems) { + StudentListItem(it, actionHandler) + } + } + + PullRefreshIndicator( + refreshing = uiState.isLoading, + state = pullRefreshState, + modifier = Modifier + .align(Alignment.TopCenter) + .testTag("pullRefreshIndicator"), + contentColor = Color(ThemePrefs.primaryColor) + ) + } +} + +@Composable +private fun StudentListItem( + uiState: StudentItemUiState, + actionHandler: (ManageStudentsAction) -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable { + actionHandler(ManageStudentsAction.StudentTapped(uiState.studentId)) + } + .padding(top = 16.dp, bottom = 16.dp, start = 16.dp) + .testTag("studentListItem"), + verticalAlignment = Alignment.CenterVertically + ) { + UserAvatar( + imageUrl = uiState.avatarUrl, + name = uiState.studentName, + modifier = Modifier.size(40.dp) + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = buildAnnotatedString { + append(uiState.studentName) + if (!uiState.studentPronouns.isNullOrEmpty()) { + withStyle(style = SpanStyle(fontStyle = FontStyle.Italic)) { + append(" (${uiState.studentPronouns})") + } + } + }, + color = colorResource(id = R.color.textDarkest), + fontSize = 16.sp + ) + Spacer(modifier = Modifier.weight(1f)) + val changeColorContentDescription = stringResource(id = R.string.changeStudentColorLabel, uiState.studentName) + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(48.dp) + .clip(CircleShape) + .testTag("studentColor") + .semantics(mergeDescendants = true) { + contentDescription = changeColorContentDescription + } + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(color = Color(uiState.studentColor.backgroundColor())) + ) { + actionHandler(ManageStudentsAction.ShowColorPickerDialog(uiState.studentId, uiState.studentColor)) + } + ) { + Box( + modifier = Modifier + .size(20.dp) + .clip(CircleShape) + .background(color = Color(uiState.studentColor.backgroundColor())) + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun ManageStudentsPreview() { + ContextKeeper.appContext = LocalContext.current + ManageStudentsScreen( + uiState = ManageStudentsUiState( + isLoading = false, + studentListItems = listOf( + StudentItemUiState(studentId = 1, studentName = "Student 1", studentPronouns = "They/Them"), + StudentItemUiState(studentId = 2, studentName = "Student 2"), + StudentItemUiState(studentId = 3, studentName = "Student 3"), + ) + ), + actionHandler = {}, + navigationActionClick = {} + ) +} + +@Preview(showBackground = true) +@Composable +private fun ManageStudentsEmptyPreview() { + ContextKeeper.appContext = LocalContext.current + ManageStudentsScreen( + uiState = ManageStudentsUiState( + isLoading = false, + studentListItems = emptyList() + ), + actionHandler = {}, + navigationActionClick = {} + ) +} + +@Preview(showBackground = true) +@Composable +private fun ManageStudentsErrorPreview() { + ContextKeeper.appContext = LocalContext.current + ManageStudentsScreen( + uiState = ManageStudentsUiState( + isLoadError = true + ), + actionHandler = {}, + navigationActionClick = {} + ) +} \ No newline at end of file diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentsUiState.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentsUiState.kt new file mode 100644 index 0000000000..443778be57 --- /dev/null +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/ManageStudentsUiState.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.parentapp.features.managestudents + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import com.instructure.pandautils.utils.ThemedColor + + +data class ManageStudentsUiState( + val isLoading: Boolean = false, + val isLoadError: Boolean = false, + val studentListItems: List = emptyList(), + val colorPickerDialogUiState: ColorPickerDialogUiState = ColorPickerDialogUiState() +) + +data class UserColor( + val colorRes: Int = 0, + val color: ThemedColor = ThemedColor(Color.Black.toArgb()), + val contentDescriptionRes: Int = 0 +) + +data class ColorPickerDialogUiState( + val showColorPickerDialog: Boolean = false, + val studentId: Long = 0, + val initialUserColor: UserColor? = null, + val userColors: List = emptyList(), + val isSavingColor: Boolean = false, + val isSavingColorError: Boolean = false +) + +data class StudentItemUiState( + val studentId: Long = 0, + val avatarUrl: String? = null, + val studentName: String = "", + val studentPronouns: String? = null, + val studentColor: ThemedColor = ThemedColor(Color.Black.toArgb()) +) + +sealed class ManageStudentsAction { + data class StudentTapped(val studentId: Long) : ManageStudentsAction() + data object Refresh : ManageStudentsAction() + data object AddStudent : ManageStudentsAction() + data class ShowColorPickerDialog(val studentId: Long, val studentColor: ThemedColor) : ManageStudentsAction() + data object HideColorPickerDialog : ManageStudentsAction() + data class StudentColorChanged(val studentId: Long, val userColor: UserColor) : ManageStudentsAction() +} + +sealed class ManageStudentsViewModelAction { + data class NavigateToAlertSettings(val studentId: Long) : ManageStudentsViewModelAction() +} diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/StudentColorPickerDialog.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/StudentColorPickerDialog.kt new file mode 100644 index 0000000000..e54352b8ce --- /dev/null +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/managestudents/StudentColorPickerDialog.kt @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.parentapp.features.managestudents + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import com.instructure.canvasapi2.utils.ContextKeeper +import com.instructure.pandautils.R +import com.instructure.pandautils.utils.ColorKeeper +import com.instructure.pandautils.utils.ThemePrefs +import com.instructure.pandautils.utils.createThemedColor + + +@Composable +internal fun StudentColorPickerDialog( + initialUserColor: UserColor?, + userColors: List, + saving: Boolean, + error: Boolean, + onDismiss: () -> Unit, + onColorSelected: (UserColor) -> Unit, + modifier: Modifier = Modifier +) { + var selected by remember { mutableStateOf(initialUserColor) } + + Dialog( + onDismissRequest = { + onDismiss() + } + ) { + Surface( + modifier = modifier, + shape = MaterialTheme.shapes.medium, + color = colorResource(id = R.color.backgroundLightestElevated) + ) { + Column { + Text( + text = stringResource(id = R.string.selectStudentColor), + color = colorResource(id = R.color.textDarkest), + fontSize = 18.sp, + modifier = Modifier.padding(20.dp) + ) + LazyRow( + horizontalArrangement = Arrangement.spacedBy(4.dp), + contentPadding = PaddingValues(horizontal = 12.dp) + ) { + items(userColors) { + val colorContentDescription = stringResource(id = it.contentDescriptionRes) + val selectedContentDescription = stringResource(id = R.string.selectedListItem, colorContentDescription) + Box( + modifier = Modifier + .size(48.dp) + .let { modifier -> + if (selected == it) { + modifier + .border(3.dp, Color(it.color.backgroundColor()), CircleShape) + .semantics { + contentDescription = selectedContentDescription + } + } else { + modifier.semantics { + contentDescription = colorContentDescription + } + } + } + .padding(8.dp) + .clip(shape = CircleShape) + .background(color = Color(it.color.backgroundColor())) + .clickable { + selected = it + } + ) + } + } + if (error) { + Text( + text = stringResource(id = R.string.errorSavingColor), + color = colorResource(id = R.color.textDanger), + fontSize = 16.sp, + modifier = Modifier.padding(start = 20.dp, end = 20.dp, top = 16.dp) + ) + } + Row( + modifier = Modifier.padding(end = 8.dp) + ) { + Spacer(modifier = Modifier.weight(1f)) + if (saving) { + Column( + modifier = Modifier.padding(16.dp) + ) { + CircularProgressIndicator( + color = Color(ThemePrefs.textButtonColor), + strokeWidth = 3.dp, + modifier = Modifier.size(32.dp) + ) + } + } else { + TextButton( + onClick = { + onDismiss() + } + ) { + Text( + text = stringResource(id = R.string.cancel), + color = Color(ThemePrefs.textButtonColor) + ) + } + TextButton( + onClick = { + if (selected == null || selected == initialUserColor) { + onDismiss() + } else { + onColorSelected(selected!!) + } + }, + ) { + Text( + text = stringResource(id = R.string.ok), + color = Color(ThemePrefs.textButtonColor), + ) + } + } + } + } + } + } +} + +@Preview +@Composable +fun StudentColorPickerDialogPreview() { + val context = LocalContext.current + ContextKeeper.appContext = context + val colors = ColorKeeper.userColors.map { + UserColor( + colorRes = it, + color = createThemedColor(context.getColor(it)), + contentDescriptionRes = 0 + ) + } + + StudentColorPickerDialog( + initialUserColor = colors.first(), + userColors = colors, + error = false, + saving = false, + onDismiss = {}, + onColorSelected = {} + ) +} diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/managestudents/ManageStudentsRepositoryTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/managestudents/ManageStudentsRepositoryTest.kt new file mode 100644 index 0000000000..d0550cc0e4 --- /dev/null +++ b/apps/parent/src/test/java/com/instructure/parentapp/features/managestudents/ManageStudentsRepositoryTest.kt @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.parentapp.features.managestudents + +import com.instructure.canvasapi2.apis.EnrollmentAPI +import com.instructure.canvasapi2.apis.UserAPI +import com.instructure.canvasapi2.models.ColorChangeResponse +import com.instructure.canvasapi2.models.Enrollment +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.LinkHeaders +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Assert +import org.junit.Test + + +class ManageStudentsRepositoryTest { + + private val enrollmentApi: EnrollmentAPI.EnrollmentInterface = mockk(relaxed = true) + private val userApi: UserAPI.UsersInterface = mockk(relaxed = true) + + private val repository = ManageStudentsRepository(enrollmentApi, userApi) + + @Test + fun `Get students successfully returns data`() = runTest { + val expected = listOf(User(id = 1L)) + val enrollments = expected.map { Enrollment(observedUser = it) } + + coEvery { enrollmentApi.firstPageObserveeEnrollmentsParent(any()) } returns DataResult.Success(enrollments) + + val result = repository.getStudents(true) + Assert.assertEquals(expected, result) + } + + @Test + fun `Get students with pagination successfully returns data`() = runTest { + val page1 = listOf(User(id = 1L)) + val enrollments1 = page1.map { Enrollment(observedUser = it) } + val page2 = listOf(User(id = 2L)) + val enrollments2 = page2.map { Enrollment(observedUser = it) } + + coEvery { enrollmentApi.firstPageObserveeEnrollmentsParent(any()) } returns DataResult.Success( + enrollments1, + linkHeaders = LinkHeaders(nextUrl = "page_2_url") + ) + coEvery { enrollmentApi.getNextPage("page_2_url", any()) } returns DataResult.Success(enrollments2) + + val result = repository.getStudents(true) + Assert.assertEquals(page1 + page2, result) + } + + @Test + fun `Get students returns data distinctly and sorted`() = runTest { + val expected = listOf(User(id = 1L, sortableName = "First"), User(id = 2L, sortableName = "Second")) + val enrollments = expected.asReversed().map { Enrollment(observedUser = it) } + val otherEnrollments = listOf( + Enrollment(user = User(id = 3L)), + Enrollment(observedUser = User(id = 1L)) + ) + + coEvery { enrollmentApi.firstPageObserveeEnrollmentsParent(any()) } returns DataResult.Success(enrollments + otherEnrollments) + + val result = repository.getStudents(true) + Assert.assertEquals(expected, result) + } + + @Test + fun `Get students returns empty list when enrollments call fails`() = runTest { + coEvery { enrollmentApi.firstPageObserveeEnrollmentsParent(any()) } returns DataResult.Fail() + + val result = repository.getStudents(true) + Assert.assertTrue(result.isEmpty()) + } + + @Test + fun `Save student color successfully returns data`() = runTest { + val contextId = "user_1" + val color = "#000000" + val expected = ColorChangeResponse(hexCode = color) + + coEvery { userApi.setColor(contextId, color, any()) } returns DataResult.Success(expected) + + val result = repository.saveStudentColor(contextId, color) + Assert.assertEquals(color, result) + } + + @Test + fun `Save student color returns null when call fails`() = runTest { + val contextId = "user_1" + val color = "000000" + + coEvery { userApi.setColor(contextId, color, any()) } returns DataResult.Fail() + + val result = repository.saveStudentColor(contextId, color) + Assert.assertNull(result) + } +} diff --git a/apps/parent/src/test/java/com/instructure/parentapp/features/managestudents/ManageStudentsViewModelTest.kt b/apps/parent/src/test/java/com/instructure/parentapp/features/managestudents/ManageStudentsViewModelTest.kt new file mode 100644 index 0000000000..d4ea8b7c12 --- /dev/null +++ b/apps/parent/src/test/java/com/instructure/parentapp/features/managestudents/ManageStudentsViewModelTest.kt @@ -0,0 +1,269 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.instructure.parentapp.features.managestudents + +import android.content.Context +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import com.instructure.canvasapi2.models.User +import com.instructure.canvasapi2.utils.ContextKeeper +import com.instructure.pandautils.utils.ColorKeeper +import com.instructure.pandautils.utils.ColorUtils +import com.instructure.pandautils.utils.ThemedColor +import com.instructure.pandautils.utils.createThemedColor +import com.instructure.parentapp.R +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.spyk +import io.mockk.unmockkObject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Rule +import org.junit.Test + + +@ExperimentalCoroutinesApi +class ManageStudentsViewModelTest { + + @get:Rule + val instantExecutorRule = InstantTaskExecutorRule() + + private val lifecycleOwner: LifecycleOwner = mockk(relaxed = true) + private val lifecycleRegistry = LifecycleRegistry(lifecycleOwner) + private val testDispatcher = UnconfinedTestDispatcher() + + private val context: Context = mockk(relaxed = true) + private val repository: ManageStudentsRepository = mockk(relaxed = true) + private val colorKeeper: ColorKeeper = spyk() + + private lateinit var viewModel: ManageStudentViewModel + + @Before + fun setup() { + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) + Dispatchers.setMain(testDispatcher) + ContextKeeper.appContext = context + mockkObject(ColorUtils) + every { ColorUtils.correctContrastForText(any(), any()) } answers { firstArg() } + every { ColorUtils.correctContrastForButtonBackground(any(), any(), any()) } answers { firstArg() } + every { context.getColor(any()) } answers { firstArg() } + every { createThemedColor(any()) } answers { ThemedColor(firstArg()) } + } + + @After + fun tearDown() { + Dispatchers.resetMain() + unmockkObject(ColorUtils) + } + + @Test + fun `Load students`() { + val students = listOf(User(id = 1, shortName = "Student 1", pronouns = "He/Him")) + val expectedState = ManageStudentsUiState( + studentListItems = listOf( + StudentItemUiState(1, null, "Student 1", "He/Him", ThemedColor(1)) + ) + ) + coEvery { repository.getStudents(any()) } returns students + coEvery { colorKeeper.getOrGenerateUserColor(any()) } returns ThemedColor(1) + + createViewModel() + + Assert.assertEquals(expectedState, viewModel.uiState.value) + } + + @Test + fun `Load students error`() { + val expectedState = ManageStudentsUiState(isLoadError = true) + coEvery { repository.getStudents(any()) } throws Exception() + + createViewModel() + + Assert.assertEquals(expectedState, viewModel.uiState.value) + } + + @Test + fun `Load students empty`() { + val expectedState = ManageStudentsUiState(isLoading = false, isLoadError = false, studentListItems = emptyList()) + coEvery { repository.getStudents(any()) } returns emptyList() + + createViewModel() + + Assert.assertEquals(expectedState, viewModel.uiState.value) + } + + @Test + fun `Navigate to alert settings screen`() = runTest { + createViewModel() + + val events = mutableListOf() + backgroundScope.launch(testDispatcher) { + viewModel.events.toList(events) + } + + viewModel.handleAction(ManageStudentsAction.StudentTapped(1L)) + + val expected = ManageStudentsViewModelAction.NavigateToAlertSettings(1L) + Assert.assertEquals(expected, events.last()) + } + + @Test + fun `Refresh reloads students`() { + createViewModel() + + viewModel.handleAction(ManageStudentsAction.Refresh) + + coVerify { repository.getStudents(true) } + } + + @Test + fun `Show color picker dialog`() { + val userColors = listOf( + UserColor( + colorRes = R.color.studentBlue, + color = ThemedColor(R.color.studentBlue), + contentDescriptionRes = R.string.studentColorContentDescriptionBlue + ), + UserColor( + colorRes = R.color.studentPurple, + color = ThemedColor(R.color.studentPurple), + contentDescriptionRes = R.string.studentColorContentDescriptionPurple + ), + UserColor( + colorRes = R.color.studentPink, + color = ThemedColor(R.color.studentPink), + contentDescriptionRes = R.string.studentColorContentDescriptionPink + ), + UserColor( + colorRes = R.color.studentRed, + color = ThemedColor(R.color.studentRed), + contentDescriptionRes = R.string.studentColorContentDescriptionRed + ), + UserColor( + colorRes = R.color.studentOrange, + color = ThemedColor(R.color.studentOrange), + contentDescriptionRes = R.string.studentColorContentDescriptionOrange + ), + UserColor( + colorRes = R.color.studentGreen, + color = ThemedColor(R.color.studentGreen), + contentDescriptionRes = R.string.studentColorContentDescriptionGreen + ), + ) + + createViewModel() + + val initialUserColor = userColors[2] + viewModel.handleAction(ManageStudentsAction.ShowColorPickerDialog(1L, initialUserColor.color)) + + val expected = ManageStudentsUiState( + colorPickerDialogUiState = ColorPickerDialogUiState( + showColorPickerDialog = true, + studentId = 1L, + initialUserColor = initialUserColor, + userColors = userColors + ) + ) + + Assert.assertEquals(expected, viewModel.uiState.value) + } + + @Test + fun `Hide color picker dialog`() = runTest { + every { colorKeeper.userColors } returns emptyList() + + createViewModel() + + viewModel.handleAction(ManageStudentsAction.ShowColorPickerDialog(1L, ThemedColor(1))) + Assert.assertTrue(viewModel.uiState.value.colorPickerDialogUiState.showColorPickerDialog) + + viewModel.handleAction(ManageStudentsAction.HideColorPickerDialog) + Assert.assertFalse(viewModel.uiState.value.colorPickerDialogUiState.showColorPickerDialog) + } + + @Test + fun `Save student color`() { + val expectedUiState = ManageStudentsUiState( + colorPickerDialogUiState = ColorPickerDialogUiState(), + studentListItems = listOf( + StudentItemUiState(1, null, "Student 1", null, ThemedColor(2)) + ) + ) + val selectedUserColor = UserColor( + colorRes = R.color.studentBlue, + color = ThemedColor(2), + contentDescriptionRes = R.string.studentColorContentDescriptionBlue + ) + + every { colorKeeper.getOrGenerateUserColor(any()) } returns ThemedColor(1) + coEvery { repository.getStudents(any()) } returns listOf(User(id = 1, shortName = "Student 1")) + coEvery { repository.saveStudentColor(any(), any()) } returns "#000000" + every { ContextCompat.getColor(context, any()) } answers { firstArg() } + + createViewModel() + + viewModel.handleAction(ManageStudentsAction.StudentColorChanged(1L, selectedUserColor)) + + Assert.assertEquals(expectedUiState, viewModel.uiState.value) + } + + @Test + fun `Save student color error`() { + val expectedUiState = ManageStudentsUiState( + colorPickerDialogUiState = ColorPickerDialogUiState(isSavingColorError = true), + studentListItems = listOf( + StudentItemUiState(1, null, "Student 1", null, ThemedColor(1)) + ) + ) + val selectedUserColor = UserColor( + colorRes = R.color.studentBlue, + color = ThemedColor(1), + contentDescriptionRes = R.string.studentColorContentDescriptionBlue + ) + + every { colorKeeper.getOrGenerateUserColor(any()) } returns ThemedColor(1) + coEvery { repository.getStudents(any()) } returns listOf(User(id = 1, shortName = "Student 1")) + every { ContextCompat.getColor(context, any()) } answers { firstArg() } + coEvery { repository.saveStudentColor(any(), any()) } throws Exception() + + createViewModel() + + viewModel.handleAction(ManageStudentsAction.StudentColorChanged(1L, selectedUserColor)) + + Assert.assertEquals(expectedUiState, viewModel.uiState.value) + } + + private fun createViewModel() { + viewModel = ManageStudentViewModel(context, colorKeeper, repository) + } +} diff --git a/buildSrc/src/main/java/GlobalDependencies.kt b/buildSrc/src/main/java/GlobalDependencies.kt index fef5638e5a..dab5a1e5e0 100644 --- a/buildSrc/src/main/java/GlobalDependencies.kt +++ b/buildSrc/src/main/java/GlobalDependencies.kt @@ -179,6 +179,7 @@ object Libs { const val COMPOSE_UI_TEST = "androidx.compose.ui:ui-test-junit4:1.6.4" const val COMPOSE_UI_TEST_MANIFEST = "androidx.compose.ui:ui-test-manifest" const val COMPOSE_ACTIVITY = "androidx.activity:activity-compose:1.8.2" + const val COMPOSE_GLIDE = "com.github.bumptech.glide:compose:1.0.0-beta01" // Navigation const val NAVIGATION_FRAGMENT = "androidx.navigation:navigation-fragment-ktx:${Versions.NAVIGATION}" diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UserAPI.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UserAPI.kt index 012f82fcd5..4f121bb0ab 100644 --- a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UserAPI.kt +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/apis/UserAPI.kt @@ -71,6 +71,13 @@ object UserAPI { @PUT("users/self/colors/{context_id}") fun setColor(@Path("context_id") contextId: String, @Query(value = "hexcode") color: String): Call + @PUT("users/self/colors/{context_id}") + suspend fun setColor( + @Path("context_id") contextId: String, + @Query(value = "hexcode") color: String, + @Tag restParams: RestParams + ): DataResult + @PUT("users/self") fun updateUserShortName(@Query("user[short_name]") shortName: String, @Body body: String): Call diff --git a/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/ColorChangeResponse.kt b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/ColorChangeResponse.kt new file mode 100644 index 0000000000..95b3ad6b76 --- /dev/null +++ b/libs/canvas-api-2/src/main/java/com/instructure/canvasapi2/models/ColorChangeResponse.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.canvasapi2.models + +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize + +@Parcelize +data class ColorChangeResponse( + @SerializedName("hexcode") + val hexCode: String? +) : CanvasModel() diff --git a/libs/pandares/src/main/res/values-night/colors.xml b/libs/pandares/src/main/res/values-night/colors.xml index 83faf3ba61..e6bd848c2c 100644 --- a/libs/pandares/src/main/res/values-night/colors.xml +++ b/libs/pandares/src/main/res/values-night/colors.xml @@ -88,10 +88,6 @@ #FFF2581B #FFF2422E - - #FF7667D5 - #FFEC3349 - @color/textDarkest @color/textDarkest diff --git a/libs/pandares/src/main/res/values/colors.xml b/libs/pandares/src/main/res/values/colors.xml index 3e29547218..292b2d1947 100644 --- a/libs/pandares/src/main/res/values/colors.xml +++ b/libs/pandares/src/main/res/values/colors.xml @@ -121,9 +121,13 @@ #FFF2581B #FFF2422E - - #FF5F4DCE - #FFE9162E + + #FF007BC2 + #FF5F4DCE + #FFBF32A4 + #FFE9162E + #FFD34503 + #FF008A12 #2ca3de diff --git a/libs/pandares/src/main/res/values/strings.xml b/libs/pandares/src/main/res/values/strings.xml index 89b4ce4594..93f8374e6e 100644 --- a/libs/pandares/src/main/res/values/strings.xml +++ b/libs/pandares/src/main/res/values/strings.xml @@ -1778,4 +1778,15 @@ Undo Failed to undo alert dismissal Failed to dismiss alert + There was an error loading your students. + Add new student + Select Student Color + An error occurred while saving your selection. Please try again. + Change color for %s + Electric, Blue + Plum, Purple + Barney, Fuschia + Raspberry, Red + Fire, Orange + Shamrock, Green diff --git a/libs/pandautils/build.gradle b/libs/pandautils/build.gradle index 9c9316e348..4f55c83753 100644 --- a/libs/pandautils/build.gradle +++ b/libs/pandautils/build.gradle @@ -218,6 +218,7 @@ dependencies { api Libs.COMPOSE_VIEW_MODEL api Libs.COMPOSE_UI api Libs.COMPOSE_ACTIVITY + api Libs.COMPOSE_GLIDE implementation Libs.FLEXBOX_LAYOUT diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/EmptyContent.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/EmptyContent.kt index 64c638e09f..bf8dc1484e 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/EmptyContent.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/EmptyContent.kt @@ -42,10 +42,10 @@ import com.instructure.pandautils.R @Composable fun EmptyContent( - emptyTitle: String, emptyMessage: String, @DrawableRes imageRes: Int, modifier: Modifier = Modifier, + emptyTitle: String? = null, buttonText: String? = null, buttonClick: (() -> Unit)? = null ) { @@ -62,16 +62,18 @@ fun EmptyContent( .testTag(imageRes.toString()) ) Spacer(modifier = Modifier.height(32.dp)) - Text( - text = emptyTitle, - fontSize = 20.sp, - color = colorResource( - id = R.color.textDarkest - ), - textAlign = TextAlign.Center, - modifier = Modifier.padding(horizontal = 32.dp) - ) - Spacer(modifier = Modifier.height(16.dp)) + emptyTitle?.let { + Text( + text = emptyTitle, + fontSize = 20.sp, + color = colorResource( + id = R.color.textDarkest + ), + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 32.dp) + ) + Spacer(modifier = Modifier.height(16.dp)) + } Text( text = emptyMessage, fontSize = 16.sp, diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/UserAvatar.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/UserAvatar.kt new file mode 100644 index 0000000000..120e889874 --- /dev/null +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/compose/composables/UserAvatar.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package com.instructure.pandautils.compose.composables + +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.graphics.drawable.toBitmap +import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi +import com.bumptech.glide.integration.compose.GlideImage +import com.instructure.pandautils.R +import com.instructure.pandautils.utils.ProfileUtils + + +@OptIn(ExperimentalGlideComposeApi::class) +@Composable +fun UserAvatar( + imageUrl: String?, + name: String, + modifier: Modifier = Modifier +) { + val model: Any? = if (ProfileUtils.shouldLoadAltAvatarImage(imageUrl)) { + ProfileUtils.createAvatarDrawable( + context = LocalContext.current, + userName = name, + borderWidth = with(LocalDensity.current) { + dimensionResource(id = R.dimen.avatar_border_width_thin).toPx().toInt() + } + ).toBitmap() + } else { + imageUrl + } + + GlideImage( + model = model, + contentDescription = null, + modifier = modifier.clip(CircleShape), + contentScale = ContentScale.Crop + ) { + it.placeholder(R.drawable.recipient_avatar_placeholder) + } +} + +@Preview(showBackground = true) +@Composable +fun UserAvatarPreview() { + UserAvatar( + imageUrl = null, + name = "Test User", + modifier = Modifier.size(100.dp) + ) +} diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ColorKeeper.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ColorKeeper.kt index 615e3be4d4..438aa71083 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ColorKeeper.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ColorKeeper.kt @@ -84,18 +84,18 @@ object ColorKeeper : PrefManager(PREFERENCE_FILE_NAME) { } } + val userColors = listOf( + R.color.studentBlue, + R.color.studentPurple, + R.color.studentPink, + R.color.studentRed, + R.color.studentOrange, + R.color.studentGreen + ) + private fun generateUserColor(user: User): ThemedColor { - val colors = listOf( - R.color.electric, - R.color.jeffGoldplum, - R.color.barney, - R.color.raspberry, - R.color.fire, - R.color.shamrock - ) - - val index = user.id.absoluteValue % colors.size - val color = ContextCompat.getColor(ContextKeeper.appContext, colors[index.toInt()]) + val index = user.id.absoluteValue % userColors.size + val color = ContextCompat.getColor(ContextKeeper.appContext, userColors[index.toInt()]) val themedColor = createThemedColor(color) cachedThemedColors += user.contextId to themedColor return themedColor @@ -250,7 +250,7 @@ object ColorApiHelper { suspend fun awaitSync(): Boolean = suspendCancellableCoroutine { cr -> performSync { cr.resumeSafely(it) } } } -private fun createThemedColor(@ColorInt color: Int): ThemedColor { +fun createThemedColor(@ColorInt color: Int): ThemedColor { val light = ColorUtils.correctContrastForText(color, ContextKeeper.appContext.getColor(R.color.white)) val darkBackgroundColor = ColorUtils.correctContrastForButtonBackground(color, ContextKeeper.appContext.getColor(R.color.backgroundDarkMode), ContextKeeper.appContext.getColor(R.color.white)) val darkTextAndIconColor = ColorUtils.correctContrastForText(color, ContextKeeper.appContext.getColor(R.color.elevatedDarkColor)) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ProfileUtils.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ProfileUtils.kt index 71ab242fe7..794b1551e3 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ProfileUtils.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/utils/ProfileUtils.kt @@ -31,7 +31,7 @@ import androidx.core.content.ContextCompat import com.instructure.canvasapi2.models.BasicUser import com.instructure.canvasapi2.models.Conversation import com.instructure.pandautils.R -import java.util.* +import java.util.Locale object ProfileUtils { @@ -72,7 +72,7 @@ object ProfileUtils { } } - private fun createAvatarDrawable(context: Context, userName: String, @Dimension borderWidth: Int): Drawable { + fun createAvatarDrawable(context: Context, userName: String, @Dimension borderWidth: Int): Drawable { val initials = getUserInitials(userName) val color = ContextCompat.getColor(context, R.color.textDark) return TextDrawable.builder() From e81dc6bb640e4ff4caa83b2103947f2f06a4c3d2 Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Mon, 29 Jul 2024 11:56:40 +0200 Subject: [PATCH 19/50] =?UTF-8?q?[MBL-17778][Student]=20-=C2=A0Show=20'No?= =?UTF-8?q?=20Internet=20Connection'=20dialog=20when=20click=20on=20Anonym?= =?UTF-8?q?ous=20discussion=20details=20'Open=20in=20browser'=20link=20(#2?= =?UTF-8?q?508)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../details/DiscussionDetailsFragment.kt | 58 ++++++++++++++++--- 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/apps/student/src/main/java/com/instructure/student/features/discussion/details/DiscussionDetailsFragment.kt b/apps/student/src/main/java/com/instructure/student/features/discussion/details/DiscussionDetailsFragment.kt index bcc9257472..f9375ebec8 100644 --- a/apps/student/src/main/java/com/instructure/student/features/discussion/details/DiscussionDetailsFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/discussion/details/DiscussionDetailsFragment.kt @@ -33,13 +33,29 @@ import androidx.lifecycle.lifecycleScope import com.google.android.material.snackbar.Snackbar import com.instructure.canvasapi2.StatusCallback import com.instructure.canvasapi2.managers.DiscussionManager -import com.instructure.canvasapi2.models.* -import com.instructure.canvasapi2.utils.* +import com.instructure.canvasapi2.models.Assignment +import com.instructure.canvasapi2.models.CanvasContext +import com.instructure.canvasapi2.models.Course +import com.instructure.canvasapi2.models.CourseSettings +import com.instructure.canvasapi2.models.DiscussionEntry +import com.instructure.canvasapi2.models.DiscussionTopic +import com.instructure.canvasapi2.models.DiscussionTopicHeader +import com.instructure.canvasapi2.models.Group +import com.instructure.canvasapi2.models.RemoteFile +import com.instructure.canvasapi2.utils.ApiPrefs +import com.instructure.canvasapi2.utils.DataResult +import com.instructure.canvasapi2.utils.DateHelper +import com.instructure.canvasapi2.utils.Logger +import com.instructure.canvasapi2.utils.NumberHelper +import com.instructure.canvasapi2.utils.Pronouns +import com.instructure.canvasapi2.utils.isValid +import com.instructure.canvasapi2.utils.mapToAttachment import com.instructure.canvasapi2.utils.pageview.BeforePageView import com.instructure.canvasapi2.utils.pageview.PageView import com.instructure.canvasapi2.utils.pageview.PageViewUrlParam import com.instructure.canvasapi2.utils.pageview.PageViewUrlQuery -import com.instructure.canvasapi2.utils.weave.* +import com.instructure.canvasapi2.utils.weave.catch +import com.instructure.canvasapi2.utils.weave.tryLaunch import com.instructure.interactions.bookmarks.Bookmarkable import com.instructure.interactions.bookmarks.Bookmarker import com.instructure.interactions.router.Route @@ -51,7 +67,30 @@ import com.instructure.pandautils.binding.viewBinding import com.instructure.pandautils.discussions.DiscussionCaching import com.instructure.pandautils.discussions.DiscussionEntryHtmlConverter import com.instructure.pandautils.discussions.DiscussionUtils -import com.instructure.pandautils.utils.* +import com.instructure.pandautils.utils.BooleanArg +import com.instructure.pandautils.utils.DiscussionEntryEvent +import com.instructure.pandautils.utils.LongArg +import com.instructure.pandautils.utils.NullableParcelableArg +import com.instructure.pandautils.utils.NullableStringArg +import com.instructure.pandautils.utils.OnBackStackChangedEvent +import com.instructure.pandautils.utils.ParcelableArg +import com.instructure.pandautils.utils.ProfileUtils +import com.instructure.pandautils.utils.ThemePrefs +import com.instructure.pandautils.utils.ViewStyler +import com.instructure.pandautils.utils.getModuleItemId +import com.instructure.pandautils.utils.isAccessibilityEnabled +import com.instructure.pandautils.utils.isGroup +import com.instructure.pandautils.utils.isTablet +import com.instructure.pandautils.utils.loadHtmlWithIframes +import com.instructure.pandautils.utils.makeBundle +import com.instructure.pandautils.utils.onClick +import com.instructure.pandautils.utils.onClickWithRequireNetwork +import com.instructure.pandautils.utils.orDefault +import com.instructure.pandautils.utils.setGone +import com.instructure.pandautils.utils.setInvisible +import com.instructure.pandautils.utils.setVisible +import com.instructure.pandautils.utils.setupAsBackButton +import com.instructure.pandautils.utils.setupAvatarA11y import com.instructure.pandautils.views.CanvasWebView import com.instructure.student.BuildConfig import com.instructure.student.R @@ -61,7 +100,11 @@ import com.instructure.student.events.DiscussionUpdatedEvent import com.instructure.student.events.ModuleUpdatedEvent import com.instructure.student.events.post import com.instructure.student.features.modules.progression.CourseModuleProgressionFragment -import com.instructure.student.fragment.* +import com.instructure.student.fragment.DiscussionsReplyFragment +import com.instructure.student.fragment.DiscussionsUpdateFragment +import com.instructure.student.fragment.InternalWebviewFragment +import com.instructure.student.fragment.LtiLaunchFragment +import com.instructure.student.fragment.ParentFragment import com.instructure.student.router.RouteMatcher import com.instructure.student.util.Const import dagger.hilt.android.AndroidEntryPoint @@ -73,7 +116,7 @@ import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import java.net.URLDecoder -import java.util.* +import java.util.Date import java.util.regex.Pattern import javax.inject.Inject @@ -633,7 +676,8 @@ class DiscussionDetailsFragment : ParentFragment(), Bookmarkable { openInBrowser.setVisible(discussionTopicHeader.htmlUrl?.isNotEmpty() == true) replyToDiscussionTopic.setGone() swipeRefreshLayout.isEnabled = false - openInBrowser.onClick { + + openInBrowser.onClickWithRequireNetwork { discussionTopicHeader.htmlUrl?.let { url -> InternalWebviewFragment.loadInternalWebView( activity, From 5ed8be645fff91c00b29924bebfb7d2d888287ef Mon Sep 17 00:00:00 2001 From: inst-danger Date: Mon, 29 Jul 2024 12:34:26 +0200 Subject: [PATCH 20/50] Update translations (#2510) --- .../src/main/res/values-hi/strings.xml | 1 + .../lib/l10n/res/intl_ar.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_ca.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_cy.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_da.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_da_instk12.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_de.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_en_AU.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_en_AU_unimelb.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_en_CA.arb | 447 +++++++++++++++++ .../lib/l10n/res/intl_en_CY.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_en_GB.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_en_GB_instukhe.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_es.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_es_ES.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_fi.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_fr.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_fr_CA.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_ga.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_hi.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_ht.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_id.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_is.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_it.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_ja.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_mi.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_ms.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_nb.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_nb_instk12.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_nl.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_pl.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_pt_BR.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_pt_PT.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_ru.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_sl.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_sv.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_sv_instk12.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_th.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_vi.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_zh.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_zh_HK.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_zh_Hans.arb | 460 ++++++++++++++++++ .../lib/l10n/res/intl_zh_Hant.arb | 460 ++++++++++++++++++ .../src/main/res/values-hi/strings.xml | 111 ++++- 44 files changed, 19418 insertions(+), 1 deletion(-) create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_ar.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_ca.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_cy.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_da.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_da_instk12.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_de.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_en_AU.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_en_AU_unimelb.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_en_CA.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_en_CY.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_en_GB.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_en_GB_instukhe.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_es.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_es_ES.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_fi.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_fr.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_fr_CA.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_ga.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_hi.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_ht.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_id.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_is.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_it.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_ja.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_mi.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_ms.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_nb.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_nb_instk12.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_nl.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_pl.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_pt_BR.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_pt_PT.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_ru.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_sl.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_sv.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_sv_instk12.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_th.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_vi.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_zh.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_zh_HK.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_zh_Hans.arb create mode 100644 libs/flutter_student_embed/lib/l10n/res/intl_zh_Hant.arb diff --git a/apps/teacher/src/main/res/values-hi/strings.xml b/apps/teacher/src/main/res/values-hi/strings.xml index b9cfcdab32..9b6d7c42e0 100644 --- a/apps/teacher/src/main/res/values-hi/strings.xml +++ b/apps/teacher/src/main/res/values-hi/strings.xml @@ -898,4 +898,5 @@ माफ़ करें %s द्वारा ओवरग्रेड किया गया + यदि छात्र सबमिशन हैं, तो %s को अप्रकाशित नहीं किया जा सकता diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_ar.arb b/libs/flutter_student_embed/lib/l10n/res/intl_ar.arb new file mode 100644 index 0000000000..df47f0ed8d --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_ar.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "المساقات", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "التقويم", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "التقويمات", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "الشهر القادم: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "الشهر السابق: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "الأسبوع القادم بدءاً من {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "الأسبوع الماضي بدءاً من {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "شهر {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "توسيع", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "طي", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} من النقاط الممكنة", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}، {eventCount} حدث}other{{date}، {eventCount} أحداث}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "لا توجد أحداث اليوم!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "يبدو أنه يوم رائع للراحة والاسترخاء وتجديد النشاط.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "حدث خطأ أثناء تحميل التقويم الخاص بك", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "حدد عناصر لعرضها على التقويم.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "الذهاب إلى اليوم", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "قائمة المهام", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "لا يوجد وصف حتى الآن", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "التاريخ", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "تحرير", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "قائمة مهام جديدة", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "تحرير قائم مهام", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "العنوان", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "المساق (اختياري)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "بلا", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "الوصف", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "حفظ", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "هل أنت متأكد؟", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "هل تريد حذف عنصر قائمة المهام هذا؟", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "تغييرات غير محفوظة", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "هل ترغب بالتأكيد في إغلاق هذه الصفحة؟ ستُفقد تغييراتك غير المحفوظة.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "لا يمكن أن يكون العنوان فارغًا", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "حدث خطأ أثناء حفظ قائمة المهام هذه. يرجى التحقق من الاتصال وإعادة المحاولة.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "حدث خطأ أثناء حذف قائمة المهام هذه. يرجى التحقق من الاتصال وإعادة المحاولة.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "عذرًا!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "لا نعلم ما حدث على وجه الدقة، ولكنه لم يكن أمراً جيداً. اتصل بنا إذا استمر هذا في الحدوث.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "عرض تفاصيل الخطأ", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "إصدار التطبيق", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "طراز الجهاز", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "إصدار نظام تشغيل Android", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "رسالة الخطأ الكاملة", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "لا يوجد أي مساق", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "ربما لم يتم نشر مساقات الطالب بعد.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "حدث خطأ أثناء تحميل مساقات الطالب.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "قائمة مهام {courseName}", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "تم تقييم الدرجة", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "تم الإرسال", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} من النقاط", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "معفى", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "مفقود", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "إلغاء", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "نعم", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "لا", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "إعادة المحاولة", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "حذف", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "تم", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} في {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "تاريخ الاستحقاق {date} في {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_ca.arb b/libs/flutter_student_embed/lib/l10n/res/intl_ca.arb new file mode 100644 index 0000000000..b5e7a68d69 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_ca.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Assignatures", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Calendari", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Calendaris", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "El mes que ve: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "El mes passat: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "La setmana que ve a partir del {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "La setmana passada a partir del {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Mes de {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "desplega", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "redueix", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} punts possibles", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} esdeveniment}other{{date}, {eventCount} esdeveniments}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "Avui no hi ha cap esdeveniment.", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "Sembla un dia fabulós per descansar, relaxar-se i carregar piles.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "S'ha produït un error en carregar el calendari", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Seleccioneu els elements que es mostraran al calendari.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Vés a avui", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "Tasques pendents", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Encara no hi ha cap descripció", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Data", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Edita", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "Noves tasques pendents", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Edita les tasques pendents", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Títol", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Assignatura (opcional)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Cap", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Descripció", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Desa", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Segur?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Voleu suprimir aquest element de les Tasques pendents?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Canvis no desats", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Segur que voleu tancar aquesta pàgina? Es perdran els canvis que no hàgiu desat.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "El títol no pot estar buit", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "S'ha produït un error en desar aquestes tasques pendents. Reviseu la connexió i torneu-ho a provar.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "S'ha produït un error en carregar aquestes tasques pendents. Reviseu la connexió i torneu-ho a provar.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Oh!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "No estem segurs de què ha passat, però no ha sigut res bo. Poseu-vos en contacte amb nosaltres si us segueix passant.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Visualitza els detalls de l'error", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Versió de l'aplicació", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Model del dispositiu", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Versió del sistema operatiu Android", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Missatge d'error complet", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "No hi ha cap assignatura", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "És possible que encara no s'hagin publicat les vostres assignatures de l'estudiant.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "S'ha produït un error en carregar els vostres assignatures de l'estudiant.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "Tasques pendents de {courseName}", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Qualificat", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Entregat", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} punts", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Excusat", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "No presentat", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Cancel·la", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Sí", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "No", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Torna-ho a provar", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Suprimeix", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Fet", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} a les {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Venç el {date} a les {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_cy.arb b/libs/flutter_student_embed/lib/l10n/res/intl_cy.arb new file mode 100644 index 0000000000..a5b5d82883 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_cy.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Cyrsiau", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Calendr", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Calendrau", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Mis nesaf: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Mis blaenorol: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Wythnos nesaf yn cychwyn {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Wythnos flaenorol yn cychwyn {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Mis {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "ehangu", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "crebachu", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} pwynt yn bosib", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} digwyddiad}other{{date}, {eventCount} digwyddiad}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "Dim Digwyddiadau Heddiw!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "Mae’n edrych fel diwrnod gwych i orffwys, ymlacio a dod at eich hun.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "Gwall wrth lwytho eich calendr", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Dewiswch elfennau i’w dangos ar y calendr.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Mynd i heddiw", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "Tasgau i’w Gwneud", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Does dim disgrifiad eto", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Dyddiad", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Golygu", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "Tasg Newydd i’w Gwneud", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Golygu Tasg i’w Gwneud", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Teitl", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Cwrs (dewisol)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Dim", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Disgrifiad", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Cadw", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Ydych chi’n siŵr?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Ydych chi am ddileu'r eitem I’w Gwneud?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Newidiadau heb eu cadw", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Ydych chi’n siŵr eich bod chi eisiau cau’r dudalen hon? Byddwch chi’n colli unrhyw newidiadau sydd heb eu cadw.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Chaiff y teitl ddim bod yn wag", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "Gwall wrth gadw’r Dasg i’w Gwneud. Gwiriwch eich cysylltiad a rhowch gynnig arall arni.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "Gwall wrth ddileu’r Dasg i’w Gwneud. Gwiriwch eich cysylltiad a rhowch gynnig arall arni.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "O na!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "Dydyn ni ddim yn siŵr beth ddigwyddodd, ond doedd o ddim yn dd. Cysylltwch â ni os ydy hyn yn parhau i ddigwydd.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Gweld manylion gwall", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Fersin o’r rhaglen", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Model o’r ddyfais", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Fersiwn OS Android", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Neges gwall llawn", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "Dim Cyrsiau", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Efallai nad yw cyrsiau eich myfyriwr wedi cael eu cyhoeddi eto.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "Gwall wrth lwytho cyrsiau eich myfyriwr.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} I’w Gwneud", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Wedi graddio", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Wedi Cyflwyno", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} pwynt", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Wedi esgusodi", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Ar goll", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Canslo", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Iawn", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "Na", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Ailgynnig", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Dileu", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Wedi gorffen", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} at {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Erbyn {date} am {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_da.arb b/libs/flutter_student_embed/lib/l10n/res/intl_da.arb new file mode 100644 index 0000000000..ab70abf121 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_da.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Fag", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Kalender", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Kalendere", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Næste måned: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Forrige måned: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Næste uge, der starter {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Forrige uge, der starter {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Måneden {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "udvid", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "skjul", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} mulige point", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} begivenhed}other{{date}, {eventCount} begivenheder}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "Ingen begivenheder i dag!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "Det er en alle tiders dag til at tage den med ro og slappe af.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "Der opstod en fejl under indlæsning af din kalender", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Vælg elementer, der skal vises i kalenderen.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Gå til I dag", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "Opgaveliste", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Der er ingen beskrivelse endnu", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Dato", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Rediger", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "Nyt på opgavelisten", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Rediger opgavelisten", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Titel", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Fag (valgfrit)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Ingen", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Beskrivelse", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Gem", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Er du sikker?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Vil du slette dette element på opgavelisten?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Ikke-gemte ændringer", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Er du sikker på, at du vil lukke denne side? Dine ikke-gemte ændringer vil gå tabt.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Titel kan ikke være tom", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "Der opstod en fejl ved lagring af denne opgaveliste. Kontrollér forbindelsen, og prøv igen.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "Der opstod en fejl under sletning af denne opgaveliste. Kontrollér forbindelsen, og prøv igen.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Åh ååh!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "Vi ved ikke helt, hvad der skete, men det var ikke godt. Kontakt os, hvis dette fortsætter.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Vis fejldetaljer", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "App-version", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Enhedsmodel", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Android OS-version", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Hel fejlmeddelelse", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "Ingen kurser", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Din studerendes fag kan muligvis ikke offentliggøres endnu.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "Der opstod en fejl under indlæsningen af din studerendes fag.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} Opgaveliste", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Bedømt", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Afleveret", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} point", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Undskyldt", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Mangler", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Annullér", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Ja", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "Nej", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Prøv igen", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Slet", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Udført", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} kl. {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Forfalder d. {date} kl. {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_da_instk12.arb b/libs/flutter_student_embed/lib/l10n/res/intl_da_instk12.arb new file mode 100644 index 0000000000..5d2d33c3d4 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_da_instk12.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Fag", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Kalender", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Kalendere", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Næste måned: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Forrige måned: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Næste uge, der starter {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Forrige uge, der starter {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Måneden {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "udvid", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "skjul", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} mulige point", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} begivenhed}other{{date}, {eventCount} begivenheder}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "Ingen begivenheder i dag!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "Det er en alle tiders dag til at tage den med ro og slappe af.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "Der opstod en fejl under indlæsning af din kalender", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Vælg elementer, der skal vises i kalenderen.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Gå til I dag", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "Opgaveliste", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Der er ingen beskrivelse endnu", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Dato", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Redigér", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "Nyt på opgavelisten", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Rediger opgavelisten", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Titel", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Fag (valgfrit)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Ingen", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Beskrivelse", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Gem", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Er du sikker?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Vil du slette dette element på opgavelisten?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Ugemte ændringer", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Er du sikker på, at du vil lukke denne side? Dine ikke-gemte ændringer vil gå tabt.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Titel kan ikke være tom", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "Der opstod en fejl ved lagring af denne opgaveliste. Kontrollér forbindelsen, og prøv igen.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "Der opstod en fejl under sletning af denne opgaveliste. Kontrollér forbindelsen, og prøv igen.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Åh nej!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "Vi ved ikke helt, hvad der skete, men det var ikke godt. Kontakt os, hvis dette fortsætter.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Vis fejldetaljer", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "App-version", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Enhedsmodel", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Android OS-version", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Hel fejlmeddelelse", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "Ingen fag", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Din elevs fag kan muligvis ikke offentliggøres endnu.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "Der opstod en fejl under indlæsningen af din elevs fag.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} Opgaveliste", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Bedømt", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Afleveret", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} point", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Undskyldt", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Mangler", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Annullér", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Ja", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "Nej", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Prøv igen", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Slet", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Udført", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} kl. {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Forfalder d. {date} kl. {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_de.arb b/libs/flutter_student_embed/lib/l10n/res/intl_de.arb new file mode 100644 index 0000000000..69c7097f06 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_de.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Kurse", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Kalender", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Kalender", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Nächster Monat: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Vorheriger Monat: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Nächste Woche, beginnend am {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Vorherige Woche, beginnend am {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Monat {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "erweitern", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "reduzieren", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} Punkte möglich", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} Ereignis}other{{date}, {eventCount} Ereignisse}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "Heute keine Ereignisse!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "Scheinbar ein großartiger Tag für Ruhe, Entspannung und Energie tanken..", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "Fehler beim Laden Ihres Kalenders", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Wählen Sie Elemente aus, die im Kalender angezeigt werden sollen.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Zu „heute“ gehen", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "Zu erledigen", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Es ist noch keine Beschreibung vorhanden", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Datum", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Ändern", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "Neue Aufgabe", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Aufgabe bearbeiten", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Titel", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Kurs (optional)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Keine", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Beschreibung", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Speichern", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Sind Sie sicher?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Möchten Sie diese Aufgabe wirklich löschen?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Nicht gespeicherte Änderungen", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Möchten Sie diese Seite wirklich schließen? Alle nicht gespeicherten Daten gehen verloren.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Titel darf nicht leer sein", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "Fehler beim Speichern dieser Aufgabe Bitte überprüfen Sie Ihre Verbindung, und versuchen Sie es erneut.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "Fehler beim Löschen dieser Aufgabe. Bitte überprüfen Sie Ihre Verbindung, und versuchen Sie es erneut.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Oh je!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "Wir sind nicht sicher, was passiert ist, aber es war nicht gut. Kontaktieren Sie uns, falls dies wieder passiert.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Fehlerdetails anzeigen", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Anwendungsversion", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Gerätemodell", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Android OS-Version", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Vollständige Fehlermeldung", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "Keine Kurse", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Die Kurse sind möglicherweise noch nicht veröffentlicht.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "Fehler beim Laden der Kurse Ihrer/Ihres Studierenden.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "Aufgabe für {courseName}", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Benotet", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Abgegeben", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} Pkte.", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Entschuldigt", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Fehlt", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Abbrechen", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Ja", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "Nein", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Erneut versuchen", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Löschen", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Fertig", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "Am {date} um {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Fällig am {date} um {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_en_AU.arb b/libs/flutter_student_embed/lib/l10n/res/intl_en_AU.arb new file mode 100644 index 0000000000..0d149e4227 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_en_AU.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Courses", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Calendar", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Calendars", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Next month: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Previous month: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Next week starting {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Previous week starting {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Month of {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "expand", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "collapse", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} points possible", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} event}other{{date}, {eventCount} events}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "No Events Today!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "It looks like a great day to rest, relax, and recharge.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "There was an error loading your calendar", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Select elements to display on the calendar.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Go to today", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "To Do", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "There's no description yet", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Date", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Edit", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "New To Do", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Edit To Do", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Title", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Course (optional)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "None", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Description", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Save", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Are You Sure?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Do you want to delete this To Do item?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Unsaved changes", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Are you sure you wish to close this page? Your unsaved changes will be lost.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Title must not be empty", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "There was an error saving this To Do. Please check your connection and try again.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "There was an error deleting this To Do. Please check your connection and try again.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Uh oh!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "View error details", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Application version", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Device model", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Android OS version", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Full error message", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "No Courses", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Your students' courses might not be published yet.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "There was an error loading your students' courses.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} To Do", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Marked", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Submitted", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} pts", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Excused", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Missing", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Cancel", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Yes", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "No", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Retry", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Delete", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Done", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} at {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Due {date} at {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_en_AU_unimelb.arb b/libs/flutter_student_embed/lib/l10n/res/intl_en_AU_unimelb.arb new file mode 100644 index 0000000000..8414f8d960 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_en_AU_unimelb.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Subjects", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Calendar", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Calendars", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Next month: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Previous month: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Next week starting {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Previous week starting {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Month of {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "expand", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "collapse", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} points possible", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} event}other{{date}, {eventCount} events}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "No Events Today!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "It looks like a great day to rest, relax, and recharge.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "There was an error loading your calendar", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Select elements to display on the calendar.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Go to today", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "To Do", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "There's no description yet", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Date", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Edit", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "New To Do", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Edit To Do", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Title", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Subject (optional)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "None", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Description", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Save", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Are You Sure?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Do you want to delete this To Do item?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Unsaved changes", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Are you sure you wish to close this page? Your unsaved changes will be lost.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Title must not be empty", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "There was an error saving this To Do. Please check your connection and try again.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "There was an error deleting this To Do. Please check your connection and try again.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Uh oh!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "View error details", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Application version", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Device model", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Android OS version", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Full error message", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "No Subjects", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Your students' subjects might not be published yet.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "There was an error loading your students' subjects.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} To Do", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Graded", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Submitted", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} pts", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Excused", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Missing", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Cancel", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Yes", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "No", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Retry", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Delete", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Done", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} at {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Due {date} at {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_en_CA.arb b/libs/flutter_student_embed/lib/l10n/res/intl_en_CA.arb new file mode 100644 index 0000000000..fc40d3b962 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_en_CA.arb @@ -0,0 +1,447 @@ +{ + "@@last_modified": "2022-01-28T12:37:53.041723", + "coursesLabel": "Courses", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Calendar", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Calendars", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Next month: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Previous month: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Next week starting {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Previous week starting {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Month of {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "expand", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "collapse", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} points possible", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "No Events Today!": "No Events Today!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "It looks like a great day to rest, relax, and recharge.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "There was an error loading your calendar", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Tap to favorite the courses you want to see on the Calendar.": "Tap to favorite the courses you want to see on the Calendar.", + "@Tap to favorite the courses you want to see on the Calendar.": { + "description": "Description text on calendar filter screen.", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Go to today", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "To Do", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "There's no description yet", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Date", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Edit", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "New To Do", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Edit To Do", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Title", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Course (optional)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "None", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Description", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Save", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Are You Sure?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Do you want to delete this To Do item?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Unsaved changes", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Are you sure you wish to close this page? Your unsaved changes will be lost.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Title must not be empty", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "There was an error saving this To Do. Please check your connection and try again.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "There was an error deleting this To Do. Please check your connection and try again.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Uh oh!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "View error details", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Application version", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Device model", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Android OS version", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Full error message", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "No Courses", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Your student’s courses might not be published yet.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "There was an error loading your your student’s courses.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} To Do", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Graded", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Submitted", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} pts", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Excused", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Missing", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Cancel", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Yes", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "No", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Retry", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Delete", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Done", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} at {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Due {date} at {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_en_CY.arb b/libs/flutter_student_embed/lib/l10n/res/intl_en_CY.arb new file mode 100644 index 0000000000..6daa11570b --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_en_CY.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Modules", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Calendar", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Calendars", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Next month: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Previous month: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Next week starting {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Previous week starting {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Month of {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "expand", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "collapse", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} points possible", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} event}other{{date}, {eventCount} events}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "No Events Today!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "It looks like a great day to rest, relax, and recharge.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "There was an error loading your calendar", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Select elements to display on the calendar.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Go to today", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "To-do", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "There's no description yet", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Date", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Edit", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "New To Do", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Edit To Do", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Title", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Module (optional)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "None", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Description", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Save", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Are you sure?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Do you want to delete this To Do item?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Unsaved changes", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Are you sure you wish to close this page? Your unsaved changes will be lost.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Title must not be empty", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "There was an error saving this To Do. Please check your connection and try again.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "There was an error deleting this To Do. Please check your connection and try again.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Uh oh!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "View error details", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Application version", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Device model", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Android OS version", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Full error message", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "No Modules", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Your student’s modules might not be published yet.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "There was an error loading your student’s modules.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} To Do", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Graded", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Submitted", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} pts", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Excused", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Missing", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Cancel", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Yes", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "No", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Retry", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Delete", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Done", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} at {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Due {date} at {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_en_GB.arb b/libs/flutter_student_embed/lib/l10n/res/intl_en_GB.arb new file mode 100644 index 0000000000..1faa04e103 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_en_GB.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Courses", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Calendar", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Calendars", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Next month: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Previous month: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Next week starting {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Previous week starting {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Month of {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "expand", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "collapse", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} points possible", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} event}other{{date}, {eventCount} events}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "No Events Today!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "It looks like a great day to rest, relax, and recharge.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "There was an error loading your calendar", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Select elements to display on the calendar.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Go to today", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "To-do", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "There's no description yet", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Date", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Edit", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "New To Do", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Edit To Do", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Title", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Course (optional)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "None", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Description", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Save", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Are you sure?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Do you want to delete this To Do item?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Unsaved changes", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Are you sure you wish to close this page? Your unsaved changes will be lost.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Title must not be empty", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "There was an error saving this To Do. Please check your connection and try again.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "There was an error deleting this To Do. Please check your connection and try again.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Uh oh!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "View error details", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Application version", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Device model", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Android OS version", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Full error message", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "No Courses", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Your student’s courses might not be published yet.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "There was an error loading your student’s courses.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} To Do", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Graded", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Submitted", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} pts", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Excused", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Missing", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Cancel", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Yes", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "No", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Retry", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Delete", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Done", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} at {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Due {date} at {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_en_GB_instukhe.arb b/libs/flutter_student_embed/lib/l10n/res/intl_en_GB_instukhe.arb new file mode 100644 index 0000000000..6daa11570b --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_en_GB_instukhe.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Modules", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Calendar", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Calendars", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Next month: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Previous month: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Next week starting {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Previous week starting {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Month of {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "expand", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "collapse", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} points possible", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} event}other{{date}, {eventCount} events}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "No Events Today!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "It looks like a great day to rest, relax, and recharge.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "There was an error loading your calendar", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Select elements to display on the calendar.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Go to today", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "To-do", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "There's no description yet", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Date", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Edit", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "New To Do", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Edit To Do", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Title", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Module (optional)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "None", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Description", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Save", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Are you sure?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Do you want to delete this To Do item?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Unsaved changes", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Are you sure you wish to close this page? Your unsaved changes will be lost.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Title must not be empty", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "There was an error saving this To Do. Please check your connection and try again.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "There was an error deleting this To Do. Please check your connection and try again.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Uh oh!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "View error details", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Application version", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Device model", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Android OS version", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Full error message", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "No Modules", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Your student’s modules might not be published yet.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "There was an error loading your student’s modules.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} To Do", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Graded", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Submitted", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} pts", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Excused", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Missing", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Cancel", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Yes", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "No", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Retry", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Delete", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Done", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} at {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Due {date} at {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_es.arb b/libs/flutter_student_embed/lib/l10n/res/intl_es.arb new file mode 100644 index 0000000000..456f393c50 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_es.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Cursos", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Calendario", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Calendarios", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Próximo mes: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Mes anterior: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "La próxima semana comienza el {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "La semana anterior comienza el {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Mes de {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "expandir", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "colapsar", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} puntos posibles", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} evento}other{{date}, {eventCount} eventos}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "¡No hay ningún evento hoy!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "Este parece ser un día excelente para descansar, relajarse y recargar energías.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "Hubo un error al cargar su calendario", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Seleccionar los elementos que se mostrarán en el calendario.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Ir a la fecha de hoy", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "Por hacer", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Todavía no hay descripción", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Fecha", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Editar", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "Nueva Tarea por hacer", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Editar Tarea por hacer", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Título", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Curso (opcional)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Ninguno", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Descripción", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Guardar", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "¿Está seguro?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "¿Desea eliminar esta Tarea por hacer?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Cambios no guardados", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "¿Está seguro de que desea cerrar esta página? Sus cambios no guardados se perderán.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "El título no puede estar vacío", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "Hubo un error al guardar esta Tarea por hacer. Compruebe su conexión y vuelva a intentarlo.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "Hubo un error al eliminar esta Tarea por hacer. Compruebe su conexión y vuelva a intentarlo.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "¡Ay, no!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "No sabemos bien qué sucedió, pero no fue bueno. Comuníquese con nosotros si esto sigue sucediendo.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Ver detalles del error", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Versión de la aplicación", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Modelo del dispositivo", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Versión del SO de Android", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Mensaje de error completo", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "Sin cursos", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Es posible que los cursos de sus estudiantes aún no estén publicados.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "Hubo un error al cargar los cursos de sus estudiantes.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "Por hacer de {courseName}", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Calificado", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Entregado", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} ptos.", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Justificado", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Faltante", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Cancelar", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Sí", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "No", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Reintentar", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Eliminar", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Listo", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} a las {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Fecha límite el {date} a las {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_es_ES.arb b/libs/flutter_student_embed/lib/l10n/res/intl_es_ES.arb new file mode 100644 index 0000000000..43eef22427 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_es_ES.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Asignaturas", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Calendario", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Calendarios", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Mes siguiente: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Mes anterior: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "La próxima semana comienza el {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "La semana anterior comenzó el {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Mes de {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "expandir", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "colapsar", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} puntos posibles", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} evento}other{{date}, {eventCount} eventos}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "No hay ningún evento hoy", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "Parece un día perfecto para descansar, relajarse y recargar energías.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "Ha habido un error al cargar tu calendario", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Seleccionar los elementos para mostrar en el calendario.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Ir a la fecha de hoy", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "Tareas pendientes", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Todavía no hay descripción", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Fecha", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Editar", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "Nueva tarea pendiente", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Editar tarea pendiente", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Título", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Asignatura (opcional)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Ninguno", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Descripción", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Guardar", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "¿Estás seguro?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "¿Quieres eliminar esta tarea pendiente?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Cambios no guardados", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "¿Estás seguro de que quieres cerrar esta página? Los cambios no guardados se perderán.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "El título no puede estar vacío", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "Ha habido un error al guardar esta tarea pendiente. Comprueba tu conexión y vuelve a intentarlo.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "Ha habido un error al eliminar esta tarea pendiente. Comprueba tu conexión y vuelve a intentarlo.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "¡Ay, no!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "No sabemos qué ha ocurrido, pero no es nada bueno. Ponte en contacto con nosotros si el problema continúa.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Ver detalles del error", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Versión de la aplicación", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Modelo del dispositivo", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Versión del sistema operativo de Android", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Mensaje de error completo", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "No hay asignaturas", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Es posible que las asignaturas de tus estudiantes aún no estén publicadas.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "Ha habido un error al cargar las asignaturas de tus estudiantes.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} Tareas pendientes", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Evaluado", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Entregado", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} puntos", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Justificado", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "No presentado", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Cancelar", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Sí", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "No", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Reintentar", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Eliminar", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Hecho", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} a las {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Fecha de entrega el {date} a las {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_fi.arb b/libs/flutter_student_embed/lib/l10n/res/intl_fi.arb new file mode 100644 index 0000000000..01811b5e04 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_fi.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Kurssit", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Kalenteri", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Kalenterit", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Seuraava kuukausi: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Edellinen kuukausi: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Seuraava viikko alkaen {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Edellinen viikko alkaen {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Kuukausi {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "laajenna", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "kutista", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} mahdollista pistettä", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} tapahtuma}other{{date}, {eventCount} tapahtumaa}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "Ei tapahtumia tänään!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "Näyttää olevan hyvä päivä levätä, rentoutua ja latautua.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "Kalenterisi latauksessa ilmeni virhe", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Valitse kalenterissa näytettävät elementit.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Siirry tähän päivään", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "Tehtävälista", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Kuvausta ei vielä ole", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Päivämäärä", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Muokkaa", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "Uusi tehtävä", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Muokkaa tehtävää", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Otsikko", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Kurssi (valinnainen)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Ei mitään", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Kuvaus", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Tallenna", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Oletko varma?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Haluatko poistaa tehtäväkohteen?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Tallentamattomat muutokset", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Haluatko varmasti sulkea tämän sivun? Kaikki tallentamattomat muutokset menetetään.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Otsikko ei saa olla tyhjä", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "Tehtävän tallennuksessa tapahtui virhe. Tarkasta yhteytesi ja yritä uudelleen.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "Tämän tehtävän tallennuksessa tapahtui virhe. Tarkasta yhteytesi ja yritä uudelleen.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Voi ei!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "Emme ole varmoja, mitä tapahtui, mutta se ei ollut hyvä. Ota meihin yhteyttä, jos tätä tapahtuu edelleen.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Näytä virhetiedot", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Sovelluksen versio", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Laitteen malli", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Android-käyttöjärjestelmän versio", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Koko virhesanoma", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "Ei kursseja", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Tämän opiskelijan kursseja ei ehkä ole vielä julkaistu.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "Opiskelijasi kurssien latauksessa ilmeni virhe.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} Tehtävä", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Arvioitu", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Lähetetty", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} pistettä", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Vapautettu", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Puuttuu", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Peruuta", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Kyllä", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "Ei", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Yritä uudelleen", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Poista", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Valmis", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} kohteessa {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Määräpäivä {date} kohteessa {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_fr.arb b/libs/flutter_student_embed/lib/l10n/res/intl_fr.arb new file mode 100644 index 0000000000..c8f1da3279 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_fr.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Cours", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Agenda", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Calendriers", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Mois suivant : {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Mois précédent : {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "La semaine suivante démarre le {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "La semaine précédente démarrait le {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Mois de {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "étendre", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "réduire", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} points possibles", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} événement}other{{date}, {eventCount} événements}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "Aucun événement aujourd'hui !", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "Voilà une bien belle journée pour se reposer, se relaxer et faire le plein d'énergie.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "Une erreur est survenue lors du chargement de votre calendrier", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Sélectionnez les éléments à afficher sur le calendrier.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Aller à « Aujourd'hui »", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "À faire", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Aucune description pour le moment", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Date", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Modifier", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "Nouvelle chose à faire", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Modifier les choses à faire", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Titre", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Cours (facultatif)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Aucun", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Description", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Enregistrer", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Êtes-vous sûr ?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Voulez-vous supprimer cette chose à faire ?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Changements non enregistrés", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Voulez-vous vraiment fermer cette page ? Vos modifications non sauvegardées seront perdues.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Le titre ne peut être laissé vide", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "Une erreur est survenue lors de l’enregistrement de cette chose à faire. Veuillez vérifier votre connexion, puis réessayez.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "Une erreur est survenue lors de la suppression de cette chose à faire. Veuillez vérifier votre connexion, puis réessayez.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Oups !", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "On ne sait pas trop ce qui s’est passé, mais ça a mal fini. Contactez-nous si le problème persiste.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Afficher les détails de l’erreur", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Version de l'application", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Modèle d'appareil", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Version de l’OS Android", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Message d’erreur complet", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "Aucun cours", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Il est possible que les cours de l'élève n'aient pas encore été publiés.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "Une erreur est survenue lors du chargement des cours de l’élève.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} à faire", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Noté", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Soumis", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} pts", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Excusé", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Manquant", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Annuler", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Oui", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "Non", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Réessayer", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Supprimer", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Terminé", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "le {date} à {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Dû le {date} à {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_fr_CA.arb b/libs/flutter_student_embed/lib/l10n/res/intl_fr_CA.arb new file mode 100644 index 0000000000..f7caa53a07 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_fr_CA.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Cours", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Calendrier", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Calendriers", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Mois suivant : {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Mois précédent : {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Semaine prochaine commençant le {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Semaine précédente commençant le {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Mois de {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "développer", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "réduire", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} points possibles", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} événement}other{{date}, {eventCount} événements}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "Aucun événement d’aujourd’hui!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "C’est une belle journée pour se reposer, se détendre et recharger nos batteries.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "Une erreur est survenue lors du chargement de votre calendrier", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Sélectionnez les éléments à afficher sur le calendrier.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Aller à aujourd’hui", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "À faire", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Il n’y a pas encore de description", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Date", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Modifier", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "Nouvelle action", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Modification à faire", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Titre", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Cours (facultatif)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Aucun", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Description", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Enregistrer", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Êtes-vous certain?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Voulez-vous supprimer cette tâche À faire?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Changements non enregistrés", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Voulez-vous vraiment fermer cette page? Vos changements non enregistrés seront perdus.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Le titre ne doit pas être vide", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "Une erreur est survenue lors de l’enregistrement de cette tâche À faire. Veuillez vérifier votre connexion et réessayer.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "Une erreur est survenue lors de la suppression de cette tâche À faire. Veuillez vérifier votre connexion et réessayer.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Oh oh!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "Nous ne sommes pas sûrs de ce qui s’est passé, mais ce n’était pas bon. Contactez-nous si cela continue.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Afficher les détails de l’erreur", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Version de l’application", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Modèle de l’appareil", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Version du SE Android", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Message d’erreur complet", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "Aucun cours", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Les cours de votre étudiant peuvent ne pas être encore publiés.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "Une erreur est survenue lors du chargement des cours de votre étudiant.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} À faire", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Noté", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Envoyé", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} pts", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Exempté", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Manquant", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Annuler", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Oui", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "Non", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Réessayer", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Supprimer", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Terminé", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} à {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Dû le {date} à {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_ga.arb b/libs/flutter_student_embed/lib/l10n/res/intl_ga.arb new file mode 100644 index 0000000000..be552576c0 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_ga.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-08-25T11:04:30.842905", + "coursesLabel": "Cúrsaí", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Féilire", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Féilirí", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "An mhí seo chugainn: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "An mhí roimhe sin: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "An tseachtain seo chugainn ag tosnú {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "An tseachtain roimhe ag tosnú {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Mí {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "leathnaigh", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "leacaigh", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} pointe féideartha", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} imeacht}other{{date}, {eventCount} imeacht}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "Níl aon Imeachtaí inniu!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "Breathnaíonn sé cosúil le lá iontach chun sos a ghlacadh, scíth a ligean, agus d’anáil a tharraingt.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "Tharla earráid agus d'fhéilire á lódáil", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Roghnaigh gnéithe le taispeáint ar an bhféilire.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Téigh go dtí inniu", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "Le Déanamh", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Níl aon cur síos go fóill", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Dáta", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Cuir in eagar", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "Le Déanamh Nua", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Cuir Le Déanamh in Eagar", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Teideal", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Cúrsa (roghnach)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Ceann ar bith", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Cur síos", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Sábháil", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "An bhfuil tú cinnte?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Ar mhaith leat an mhír Le Déanamh seo a scriosadh?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Athruithe nach bhfuil sábháilte", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "An bhfuil tú cinnte gur mian leat an leathanach seo a dhúnadh? Caillfear do chuid athruithe nach bhfuil sábháilte.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Ní féidir leis an teideal a bheith folamh", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "Tharla earráid agus an Le Déanamh seo á shábháil. Seiceáil do cheangal agus bain triail eile as.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "Tharla earráid agus an Le Déanamh seo á scriosadh. Seiceáil do cheangal agus bain triail eile as.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Uh oh!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "Níl muid cinnte cad a tharla, ach ní raibh sé go maith. Déan teagmháil linn má leanann sé seo ag tarlú.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Féach ar shonraí earráide", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Leagan feidhmchláir", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Múnla gléas", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Android leagan OS", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Teachtaireacht earráide iomlán", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "Níl Cúrsaí ann", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Seans nach bhfuil cúrsaí do mhic léinn foilsithe go fóill.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "Tharla earráid agus cúrsaí do mhic léinn á lódáil.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} Le Déanamh", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Marcáilte", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Curtha isteach", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} pte", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Leithscéal Faighte", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "ar Iarraidh", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Cealaigh", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Tá", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "Níl", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Bain triail eile as", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Scrios", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Déanta", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} ag {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Dlite {date} ag {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_hi.arb b/libs/flutter_student_embed/lib/l10n/res/intl_hi.arb new file mode 100644 index 0000000000..fcde2c8db8 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_hi.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-08-25T11:04:30.842905", + "coursesLabel": "पाठ्यक्रम", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "कैलेंडर", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "कैलेंडर", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "अगला महीना: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "पिछला महीना: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "अगला सप्ताह {date} से शुरू", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "पिछला सप्ताह {date} से शुरू", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "{month} का महीना", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "बढ़ाएं", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "संक्षिप्त करें", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} पॉइंट्स संभव हैं", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} घटना}other{{date}, {eventCount} घटनाएं}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "आज कोई घटना नहीं है!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "आज का दिन आराम, विश्राम करने और नई ऊर्जा पाने के लिए अच्छा लग रहा है।", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "आपके कैलेंडर को लोड करने में त्रुटि हुई", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "कैलेंडर पर दिखाने के लिए एलीमेंट चुनें।", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "आज पर जाएं", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "करने के लिए", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "अभी तक कोई विवरण नहीं है", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "तिथि", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "संपादित करें", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "नया करने के लिए", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "करने के लिए संपादित करें", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "शीर्षक", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "पाठ्यक्रम (वैकल्पिक)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "कोई नहीं", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "विवरण", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "सहेजें", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "क्या सच में?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "क्या आप इस करने के लिए आइटम को मिटाना चाहते हैं?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "बिना सहेजे गए बदलाव", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "क्या आप सच में इस पेज को बंद करना चाहते हैं? आपके बिना सहेजे गए बदलाव गुम हो जाएंगे।", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "शीर्षक खाली नहीं होना चाहिए", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "इस करने के लिए सहेजने में त्रुटि हुई। कृपया अपने कनेक्शन की जांच करें और फिर से कोशिश करें।", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "इस करने के लिए को सहेजने में त्रुटि आई थी। कृपया अपने कनेक्शन की जांच करें और फिर से कोशिश करें।", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "उह ओह!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "हमें नहीं पता कि क्या हुआ, पर जो हुआ अच्छा नहीं था। यदि ऐसा बार-बार हो, तो हमसे संपर्क करें।", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "त्रुटि विवरण देखें", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "ऐप्लिकेशन संस्करण", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "डिवाइस मॉडल", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Android OS संस्करण", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "पूर्ण त्रुटि संदेश", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "कोई पाठ्यक्रम नहीं", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "हो सकता है कि आपके छात्र के पाठ्यक्रम अभी तक प्रकाशित न हुए हों।", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "आपके छात्र के पाठ्यक्रम लोड करने में त्रुटि हुई।", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} करने के लिए", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "ग्रेड किए गए", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "सबमिट किया गया", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} पॉइंट्स", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "माफ़ किया", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "लापता", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "रद्द करें", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "हां", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "नहीं", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "फिर से कोशिश करें", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "मिटाएं", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "हो गया", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{time} पर {date}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "{date} को {time} बजे नियत", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_ht.arb b/libs/flutter_student_embed/lib/l10n/res/intl_ht.arb new file mode 100644 index 0000000000..df331ea92c --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_ht.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Kou", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Kalandriye", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Kalandriye", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Pwochen mwa: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Mwa pase: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Semenn pwochen kòmanse {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Semenn pase kòmanse {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Mwa {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "elaji", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "ratresi", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} pwen posib", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} evènman}other{{date}, {eventCount} evènman}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "Pa gen Aktivite Jodi a!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "Sanble yon bon jou pou repoze w, amize w epi mete enèji.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "Te gen yon erè nan chajman kalandriye ou a.", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Seleksyone eleman pou afiche nan kalandriye a.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Ale nan jodi a", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "Pou Fè", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Poko gen deskripsyon", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Dat", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Modifye", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "Nouvo Tach", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Chanje Tach", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Tit", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Kou (opsyonèl)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Okenn", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Deskripsyon", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Anrejistre", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Tout bon ou vle sa?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Èske w vle efase eleman sa a nan Tach la?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Chanjman ki pa anrejistre", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Ou kwè vrèman ou vle fèmen paj sa a? W ap pèdi chanjman ki pa anrejistre yo.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Tit la pa dwe vid", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "Te gen erè pou anrejistre Tach sa a. Tanpri verifye koneksyon ou a epi eseye ankò.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "Te gen erè pou efase Tach sa a. Tanpri verifye koneksyon ou a epi eseye ankò.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Uh oh!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "Nou pa twò konnen kisa k pase a, men li pa enteresan. Pran kontak ak nou si sa repwodui.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Afiche detay erè", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Vèsyon aplikasyon", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Modèl aparèy", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Vèsyon OS Android", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Erè mesaj konplè", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "Pa gen Kou", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Kou elèv ou a ta dwe gentan pibliye.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "Te gen yon erè nan chajman kou elèv ou a.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} Tach", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Klase", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Soumèt", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} pwen", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Egzante", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Manke", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Anile", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Wi", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "Non", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Re eseye", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Efase", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Fini", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} a {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Delè {date} a {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_id.arb b/libs/flutter_student_embed/lib/l10n/res/intl_id.arb new file mode 100644 index 0000000000..50bfd8386a --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_id.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-08-25T11:04:30.842905", + "coursesLabel": "Kursus", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Kalender", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Kalender", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Bulan selanjutnya: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Bulan sebelumnya: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Minggu selanjutnya mulai {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Minggu sebelumnya mulai {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Bulan dari {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "perbesar", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "perkecil", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} poin memungkinkan", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} acara}other{{date}, {eventCount} acara}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "Tidak Ada Acara Hari Ini!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "Sepertinya hari ini sangat cocok untuk beristirahat, santai, dan menyegarkan diri.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "Terjadi kesalahan saat memuat kalender Anda", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Pilih elemen untuk ditampilkan di kalender.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Pergi ke hari ini", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "Harus Dilakukan", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Belum ada deskripsi", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Tanggal", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Edit", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "To Do Baru", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Edit To Do", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Judul", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Kursus (opsional)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Tidak ada", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Deskripsi", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Simpan", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Anda Yakin?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Anda mau menghapus Item To-do ini?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Perubahan belum disimpan", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Anda yakin mau menutup halaman ini? Perubahan yang belum disimpan akan hilang.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Judul tidak boleh kosong", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "Terjadi kesalahan saat menyimpan To Do ini. Silakan periksa sambungan internet Anda dan coba lagi.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "Terjadi kesalahan saat menghapus To Do ini. Silakan periksa sambungan internet Anda dan coba lagi.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Uh oh!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "Kami tidak yakin apa yang terjadi, tetapi hal itu tidak baik. Hubungi kami jika ini terus terjadi.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Lihat detail kesalahan", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Versi aplikasi", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Model perangkat", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Versi Android OS", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Pesan kesalahan lengkap", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "Tidak Ada Kursus", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Kursus siswa Anda mungkin belum diterbitkan.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "Terjadi kesalahan ketika memuat kursus siswa Anda.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "To Do {courseName}", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Dinilai", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Diserahkan", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} poin", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Dibolehkan", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Tidak Ada", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Batal", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Ya", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "Tidak", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Coba lagi", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Hapus", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Selesai", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} di {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Batas waktu {date} pada {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_is.arb b/libs/flutter_student_embed/lib/l10n/res/intl_is.arb new file mode 100644 index 0000000000..40ec80182c --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_is.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Námskeið", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Dagatal", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Dagatöl", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Næsti mánuður: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Fyrri mánuður: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Næsta vika hefst {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Fyrri vika hefst {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "{month} mánuði", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "víkka", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "fella saman", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} punktar mögulegir", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} viðburður}other{{date}, {eventCount} viðburðir}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "Engir viðburðir í dag!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "Þetta virðist vera góður dagur til að hvílast, slaka á og hlaða batteríin.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "Upp kom villa við að hlaða dagatalinu þínu", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Veldu einingar til að birta á dagatalinu.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Fara í daginn í dag", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "Verkefnalisti", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Engin lýsing hefur verið gerð", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Dagsetning", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Breyta", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "Nýtt verkefni", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Breyta verkefni", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Titill", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Námskeið (valfrjálst)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Ekkert", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Lýsing", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Vista", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Ertu viss?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Viltu eyða þessu verkefni?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Óvistaðar breytingar", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Viltu örugglega loka þessari síðu? Óvistaðar breytingar munu tapast.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Titillinn má ekki vera auður", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "Villa kom upp við að vista þetta verkefni. Athugaðu tengingu þína og reyndu aftur.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "Villa kom upp við að eyða þessu verkefni. Athugaðu tengingu þína og reyndu aftur.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Æi!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "Við erum ekki viss um hvað gerðist, en það var ekki gott. Hafðu samband við okkur ef þetta heldur áfram að gerast.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Skoða upplýsingar um villu", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Útgáfa forrits", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Gerð tækis", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Útgáfa Android stýrikerfis", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Heildar villuskilaboð", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "Engin námskeið", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Námskeið nemanda þíns eru ef til vill ekki birt enn.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "Villa kom upp við að sækja námskeið nemanda þíns.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} Verkefni", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Metið", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Skilað", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} punktar", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Undanþegið", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Vantar", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Hætta við", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Já", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "Nei", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Reyna aftur", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Eyða", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Lokið", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} klukkan {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Skiladagur {date} þann {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_it.arb b/libs/flutter_student_embed/lib/l10n/res/intl_it.arb new file mode 100644 index 0000000000..9a6053ab56 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_it.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Corsi", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Calendario", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Calendari", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Prossimo mese: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Mese precedente: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Prossima settimana a partire da {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Settimana precedente a partire da {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Mese di {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "estendi", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "comprimi", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} punti possibili", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} evento}other{{date}, {eventCount} eventi}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "Nessun evento oggi!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "Ottima occasione per riposarsi, rilassarsi e ricaricare le batterie.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "Si è verificato un errore durante il caricamento del tuo calendario", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Selezionare gli elementi da visualizzare sul calendario.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Vai a oggi", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "Elenco attività", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Non c’è ancora nessuna descrizione", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Data", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Modifica", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "Nuovo elenco azioni", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Modifica elenco azioni", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Titolo", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Corso (opzionale)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Nessuno", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Descrizione", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Salva", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Continuare?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Vuoi eliminare questo elemento dall’elenco azioni?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Modifiche non salvate", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Vuoi chiudere questa pagina? Le modifiche non salvate saranno perse.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Il titolo non dev’essere vuoto", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "Si è verificato un errore durante il salvataggio dell’elenco azioni. Verifica la connessione e riprova.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "Si è verificato un errore di eliminazione di questo elenco azioni. Verifica la connessione e riprova.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Spiacenti.", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "Non siamo sicuri di cos’è successo, ma non è stata una cosa positiva. Contattaci se continua a succedere.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Visualizza dettagli errori", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Versione applicazione", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Modello dispositivo", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Versione SO Android", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Messaggio di errore pieno", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "Nessun corso", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "I corsi dello studente potrebbero non essere stati ancora pubblicati.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "Si è verificato un errore durante il caricamento dei corsi dello studente.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} Elenco azioni", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Valutato", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Inviato", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} pt.", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Giustificato", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Mancante", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Annulla", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Sì", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "No", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Riprova", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Elimina", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Fatto", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "Il {date} alle {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Scade il {date} alle {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_ja.arb b/libs/flutter_student_embed/lib/l10n/res/intl_ja.arb new file mode 100644 index 0000000000..00e120e7c3 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_ja.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "コース", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "カレンダー", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "カレンダー", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "来月:{month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "前月:{month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "{date} から始まる翌週", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "{date} から始まる前週", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "{month} 月", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "さらに表示する", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "折りたたむ", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "配点 {points}", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}、{eventCount}イベント}other{{date}、{eventCount}イベント}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "今日イベントはありません!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "休息をとってリラックスし、充電するためにぴったりな日のようです。", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "カレンダー読み込み中にエラーが発生しました", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "予定表に表示するエレメントを選択します。", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "今日に進む", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "タスク", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "説明はまだありません", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "日付", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "編集", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "新しい To Do", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "To Do を編集", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "タイトル", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "コース(オプション)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "なし", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "説明", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "保存", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "ほんとうに実行しますか?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "この To Do アイテムを削除しますか?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "変更が保存されていません", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "このページを閉じてもよろしいですか?保存されていない変更はすべて失われます。", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "タイトルが空欄であることはできません", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "この To Do を保存中にエラーが起こりました。接続を確認して、もう一度お試しください。", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "この To Do を削除中にエラーが起こりました。接続を確認して、もう一度お試しください。", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "エラーです!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "何が起こったかはわかりませんが、問題が発生したようです。問題が解決されない場合は、Canvas までお問い合わせください。", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "エラーの詳細を表示", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "アプリケーションのバージョン", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "デバイスモデル", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Android OS バージョン", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "完全なエラーメッセージ", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "コースはありません", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "受講者のコースはまだ公開されていない可能性があります", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "受講者のコースのロード中にエラーが発生しました", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} To Do", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "採点済み", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "提出済み", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} 点", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "免除", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "提出なし", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "キャンセル", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "はい", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "いいえ", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "再試行", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "削除", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "終了", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date}、{time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "{date}、{time} 期限", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_mi.arb b/libs/flutter_student_embed/lib/l10n/res/intl_mi.arb new file mode 100644 index 0000000000..1ac88220fd --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_mi.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Ngā Akoranga", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Maramataka", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Ngā Maramataka", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Marama i muri mai: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Marama o muri nei: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Tīmata te wiki e heke mai nei {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Tīmata te wiki o muri {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Marama ō {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "whakawhānui", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "hinga", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} ngā koinga e taea", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} takahanga}other{{date}, {eventCount} takahanga}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "Kaore he tauwhāinga i tēnei rā!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "Ko te āhua nei he rā pai ki te whakatā me te whakahou anō.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "He hapa i puta i te wā e uta ana i tō maramataka", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Tīpakohia ngā huānga hei whakaatu ki te maramataka.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Haere ki te rā", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "Hei Mahi", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Kaore he whakturanga i tēnei wā", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Rā", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Whakatika", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "He Hou Whakamahi", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Whakatika Whakamahi", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Taitara", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Akoranga (kāre herea)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Kaore", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Whakāhuatanga", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Tiaki", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "E mōhio tūturu ana koe?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "E hiahia koe ki te muku i tēnei Hei Mahi tūemi?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Ngā whakarerekētanga kaore i tiakina", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Kei te tino hiahia koe ki te kati i tēnei whārangi? Ka ngaro ngā huringa kaore anō i tiakina.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Me noho kore te taitara", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "He hapa i te wā e tiaki ana i tenei Hei Whakamahi. Tēnā koa tirohia tō hononga ana ka tarai anō.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "He hapa i te wā e muku ana i tēnei Hei Whakamahi. Tēnā koa tirohia tō hononga ana ka tarai anō.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Aue!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "Kaore mātou i te tino mōhio he aha te mahi, ngari kaore i te pai. Whakapā mai ki a mātou mehemea ka mahi pēnei tonu tēnei.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Tirohia ngā hapa taipitopito", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Taupānga whakāturanga", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Pūrere tauira", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Android OS Putanga", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Nui karere Hapa", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "Kāore he Akoranga", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Kaore e pānuitia ō ākonga akoranga i tēnei wā.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "He hapa i te wā e uta ana i tō ākonga akoranga.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} Hei Mahi", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Kōekehia", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Kua Tukuna", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} ngā koinga", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Whakawātea", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Ngaro", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Whakakore", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Ae", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "Kahore", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Ngana anō", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Muku", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Kua mahia", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} ī {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Rā tika {date} ī {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_ms.arb b/libs/flutter_student_embed/lib/l10n/res/intl_ms.arb new file mode 100644 index 0000000000..5c2f9f1f1d --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_ms.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Kursus", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Kalendar", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Kalendar", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Bulan seterusnya: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Bulan sebelumnya: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Minggu seterusnya bermula {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Minggu sebelumnya bermula {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Bulan {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "kembangkan", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "kuncupkan", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} mata mungkin", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} acara}other{{date}, {eventCount} acara}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "Tiada Acara Hari Ini!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "Nampaknya ini hari yang baik untuk berehat, bersantai dan merawat diri.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "Terdapat ralat untuk memuatkan kalendar anda", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Pilih elemen untuk dipaparkan pada kalendar.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Terus ke hari ini", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "Untuk Dilakukan", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Belum ada penerangan", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Tarikh", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Edit", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "Senarai Untuk Dilakukan Baharu", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Edit Senarai Untuk Dilakukan", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Tajuk", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Kursus (pilihan)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Tiada", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Penerangan", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Simpan", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Adakah Anda Pasti?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Adakah anda ingin memadamkan item Untuk Dilakukan ini?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Perubahan tidak disimpan", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Adakah anda pasti anda ingin menutup halaman ini? Perubahan yang tidak disimpan akan hilang.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Tajuk tidak boleh ditinggalkan kosong", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "Terdapat ralat untuk menyimpan senarai Untuk Dilakukan ini. Sila semak sambungan anda dan cuba semula.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "Terdapat ralat untuk memadamkan senarai Untuk Dilakukan ini. Sila semak sambungan anda dan cuba semula.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Op!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "Kami tidak pasti apa yang berlaku, mungkin ada sedikit masalah. Hubungi kami jika perkara ini berlanjutan.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Lihat butiran ralat", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Versi aplikasi", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Model peranti", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Versi OS Android", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Mesej ralat penuh", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "Tiada Kursus", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Kursus pelajar anda mungkin belum diterbitkan.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "Terdapat ralat untuk memuatkan kursus pelajar anda.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "Senarai Untuk Dilakukan bagi {courseName}", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Digredkan", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Diserahkan", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} mata", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Dimaafkan", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Tiada", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Batal", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Ya", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "Tidak", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Cuba semula", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Padam", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Siap", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} di {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Cukup tempoh {date} pada {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_nb.arb b/libs/flutter_student_embed/lib/l10n/res/intl_nb.arb new file mode 100644 index 0000000000..be1ef9e45a --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_nb.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Emner", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Kalender", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Kalendere", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Neste måned: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Forrige måned: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Neste uke begynner {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Forrige uke begynte {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Måned {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "utvid", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "skjult", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} poeng oppnåelig", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} hendelse}other{{date}, {eventCount} hendelser}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "Ingen arrangementer i dag!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "Det ser ut som dette er en flott dag til å slappe av og lade batteriene.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "Det oppsto en feil under lasting av kalenderen din", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Velg elementer som skal vises i kalenderen.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Gå til idag", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "Å gjøre", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Det er ingen beskrivelse ennå", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Dato", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Rediger", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "Ny i gjøremål", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Rediger gjøremål", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Tittel", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Emne (valgfritt)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Ingen", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Beskrivelse", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Lagre", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Er du sikker?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Ønsker du å slette dette gjøremålet?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Ulagrede endringer", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Er du sikker på at du vil lukke denne siden? Dine ulagrede endringer vil gå tapt.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Tittelen kan ikke være tom", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "Det oppsto en feil ved lagring av dette gjøremålet. Kontroller tilkoblingen og prøv på nytt.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "Det oppsto en feil ved sletting av dette gjøremålet. Kontroller tilkoblingen og prøv på nytt.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Oi sann!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "Vi vet ikke hva som skjedde her, men det ser ikke bra ut. Ta kontakt med oss hvis denne situasjonen vedvarer.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Vis feildetaljer", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Applikasjonsversjon", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Enhetsmodell", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Android OS-versjon", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Fullstendig feilmelding", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "Ingen emner", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Emnene til studentene dine er kanskje upublisert enda.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "Det oppsto en feil under lasting av emnene til studentene dine.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} Gjøremål", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Vurdert", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Innlevert", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} poeng", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Fritatt", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Mangler", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Avbryt", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Ja", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "Nei", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Prøv igjen", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Slett", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Ferdig", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} på {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Frist {date} klokken {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_nb_instk12.arb b/libs/flutter_student_embed/lib/l10n/res/intl_nb_instk12.arb new file mode 100644 index 0000000000..dfda522719 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_nb_instk12.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Fag", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Kalender", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Kalendere", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Neste måned: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Forrige måned: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Neste uke begynner {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Forrige uke begynte {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Måned {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "utvid", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "skjult", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} poeng oppnåelig", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} hendelse}other{{date}, {eventCount} hendelser}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "Ingen arrangementer i dag!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "Det ser ut som dette er en flott dag til å slappe av og lade batteriene.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "Det oppsto en feil under lasting av kalenderen din", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Velg elementer som skal vises i kalenderen.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Gå til idag", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "Å gjøre", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Det er ingen beskrivelse ennå", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Dato", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Rediger", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "Ny i gjøremål", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Rediger gjøremål", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Tittel", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Fag (valgfritt)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Ingen", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Beskrivelse", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Lagre", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Er du sikker?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Ønsker du å slette dette gjøremålet?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Ulagrede endringer", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Er du sikker på at du vil lukke denne siden? Dine ulagrede endringer vil gå tapt.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Tittelen kan ikke være tom", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "Det oppsto en feil ved lagring av dette gjøremålet. Kontroller tilkoblingen og prøv på nytt.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "Det oppsto en feil ved sletting av dette gjøremålet. Kontroller tilkoblingen og prøv på nytt.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Oi sann!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "Vi vet ikke hva som skjedde her, men det ser ikke bra ut. Ta kontakt med oss hvis denne situasjonen vedvarer.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Vis feildetaljer", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Applikasjonsversjon", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Enhetsmodell", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Android OS-versjon", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Fullstendig feilmelding", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "Ingen fag", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Fagene til elevene dine er kanskje upublisert enda.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "Det oppsto en feil under lasting av fagene til elevene dine.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} Gjøremål", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Vurdert", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Innlevert", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} poeng", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Fritatt", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Mangler", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Avbryt", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Ja", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "Nei", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Forsøk igjen", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Slett", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Ferdig", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} på {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Frist {date} klokken {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_nl.arb b/libs/flutter_student_embed/lib/l10n/res/intl_nl.arb new file mode 100644 index 0000000000..30cd7251ff --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_nl.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Cursussen", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Kalender", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Kalenders", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Volgende maand: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Vorige maand: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Volgende week vanaf {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Vorige week vanaf {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "De maand {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "uitvouwen", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "samenvouwen", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} punten mogelijk", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} gebeurtenis}other{{date}, {eventCount} gebeurtenissen}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "Vandaag geen gebeurtenissen!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "Dat lijkt een prima dag om lekker uit te rusten, te relaxen en de batterij op te laden.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "Er was een probleem bij het laden van je kalender", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Selecteer elementen om weer te geven in de kalender.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Ga naar vandaag", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "Takenlijst", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Er is nog geen beschrijving", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Datum", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Bewerken", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "Nieuwe takenlijst", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Takenlijst bewerken", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Titel", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Cursus (optioneel)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Geen", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Beschrijving", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Opslaan", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Weet je het zeker?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Weet je zeker dat je dit item uit de takenlijst wilt verwijderen?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Niet-opgeslagen wijzigingen", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Weet je zeker dat je deze pagina wilt sluiten? Je niet-opgeslagen wijzigingen gaan verloren.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Titel mag niet leeg zijn", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "Er is een fout opgetreden bij het opslaan van deze takenlijst. Controleer je verbinding en probeer het opnieuw.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "Er is een fout opgetreden bij het verwijderen van deze takenlijst. Controleer je verbinding en probeer het opnieuw.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "O nee!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "We weten niet precies wat er is gebeurd, maar goed is het niet. Neem contact met ons op als dit blijft gebeuren.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Foutgegevens weergeven", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Versie van toepassing", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Apparaatmodel", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Android OS-versie", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Volledig foutbericht", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "Geen cursussen", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "De cursussen van je cursist zijn mogelijk nog niet gepubliceerd.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "Er is een fout opgetreden bij het laden van de cursussen van je cursist.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} Takenlijst", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Beoordeeld", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Ingeleverd", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} punten", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Vrijgesteld", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Ontbrekend", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Annuleren", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Ja", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "Nee", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Opnieuw proberen", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Verwijderen", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Gereed", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} om {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "In te leveren op {date} om {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_pl.arb b/libs/flutter_student_embed/lib/l10n/res/intl_pl.arb new file mode 100644 index 0000000000..6c53e7d56a --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_pl.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Kursy", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Kalendarz", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Kalendarze", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Następny miesiąc: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Poprzedni miesiąc: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Następny tydzień od {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Poprzedni tydzień od {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Miesiąc: {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "rozwiń", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "zwiń", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} pkt do zdobycia", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} zdarzenie}other{{date}, {eventCount} zdarzenia}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "Brak wydarzeń na dziś!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "Wygląda to na świetny dzień do odpoczynku, relaksu i regeneracji.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "Wystąpił błąd podczas wczytywania kalendarza", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Wybierz elementy do wyświetlenia w kalendarzu.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Przejdź do dziś", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "Lista zadań", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Brak opisu", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Data", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Edytuj", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "Nowa lista zadań", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Edytuj listę zadań", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Tytuł", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Kurs (opcjonalnie)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Brak", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Opis", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Zapisz", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Czy na pewno?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Czy na pewno chcesz usunąć ten element listy zadań?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Niezapisane zmiany", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Czy na pewno chcesz zamknąć tę stronę? Niezapisane zmiany zostaną utracone.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Tytuł nie może być pusty", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "Wystąpił błąd podczas zapisywania tej listy zadań. Sprawdź połączenie i spróbuj ponownie.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "Wystąpił błąd podczas usuwania tej listy zadań. Sprawdź połączenie i spróbuj ponownie.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "O, nie!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "Nie mamy pewności, co się wydarzyło, ale nie było to dobre. Jeśli to będzie się powtarzać, skontaktuj się z nami.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Wyświetl szczegóły błędu", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Wersja aplikacji", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Model urządzenia", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Wersja systemu Android", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Pełny komunikat o błędzie", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "Brak kursów", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Być może kursy uczestnika nie zostały jeszcze opublikowane.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "Wystąpił błąd podczas wczytywania kursów uczestnika.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "Lista zadań {courseName}", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Oceniono", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Przesłano", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} pkt", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Usprawiedliwiony", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Brak", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Anuluj", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Tak", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "Nie", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Ponów próbę", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Usuń", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Gotowe", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} o {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Termin {date} o {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_pt_BR.arb b/libs/flutter_student_embed/lib/l10n/res/intl_pt_BR.arb new file mode 100644 index 0000000000..39a2d53c87 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_pt_BR.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Cursos", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Calendário", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Calendários", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Próximo mês: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Mês anterior: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Próxima semana começando {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Semana anterior começando {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Mês de {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "expandir", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "recolher", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} pontos possíveis", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} evento}other{{date}, {eventCount} eventos}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "Nenhum evento hoje!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "Parece um bom dia para descansar, relaxar e recarregar.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "Ocorreu um erro ao carregar sua agenda", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Selecione os elementos a serem exibidos no calendário.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Ir para hoje", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "Lista de Tarefas", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Ainda não há descrição", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Data", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Editar", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "Nova lista de tarefas", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Editar lista de tarefas", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Título", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Curso (opcional)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Nenhum", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Descrição", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Salvar", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Você tem certeza?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Deseja excluir este item da lista de tarefas?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Alterações não salvas", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Tem certeza de que deseja fechar esta página? Suas alterações não salvas serão perdidas.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "O título não pode estar vazio", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "Ocorreu um erro ao salvar esta Lista de Tarefas. Verifique a sua conexão e tente novamente.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "Ocorreu um erro ao excluir esta Lista de Tarefas. Verifique a sua conexão e tente novamente.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Que difícil...", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "Não temos certeza do que aconteceu, mas não foi bom. Contate-nos se isso continuar acontecendo.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Visualizar detalhes do erro", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Versão do aplicativo", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Modelo do dispositivo", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Versão SO do Android", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Mensagem de erro completa", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "Sem Cursos", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Os cursos do seu aluno podem ainda não ter sido publicados.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "Houve um erro ao carregar os cursos do seu aluno.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} Lista de Tarefas", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Avaliado", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Enviado", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} pts", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Dispensado", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Faltante", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Cancelar", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Sim", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "Não", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Tentar novamente", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Excluir", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Feito", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} às {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Para ser entregue em {date} às {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_pt_PT.arb b/libs/flutter_student_embed/lib/l10n/res/intl_pt_PT.arb new file mode 100644 index 0000000000..3a16fe8130 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_pt_PT.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Disciplinas", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Calendário", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Calendários", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Próximo mês: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Mês anterior: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "A partir da próxima semana {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "A partir da semana anterior {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Mês de {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "expandido", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "recolhido", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} ponto possível", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} evento}other{{date}, {eventCount} eventos}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "Nenhum evento hoje!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "Parece um ótimo dia para descansar, relaxar e recarregar.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "Houve um erro ao carregar seu calendário", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Selecionar elementos a exibir no calendário.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Ir para hoje", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "A Fazer", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Ainda não há descrição", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Data", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Editar", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "Novo para fazer", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Editar para fazer", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Título", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Disciplina (opcional)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Nenhum", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Descrição", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Guardar", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Tem certeza?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Tem certeza de que deseja eliminar este item de Tarefa?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Alterações não guardadas", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Tem a certeza de que pretende fechar esta página? Todos os dados não guardados serão perdidos.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "O título não pode estar vazio", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "Ocorreu um erro ao guardar esta Tarefa. É favor verificar sua conexão e tente novamente.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "Houve um erro ao eliminar esta Tarefa É favor verificar sua conexão e tente novamente.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Ah não!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "Nós não temos certeza do que aconteceu, mas não foi bom. Contacte-nos se isto continuar a acontecer.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Ver detalhes do erro", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Versão da aplicação", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Modelo do dispositivo", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "versão do Android OS", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Mensagem de erro completa", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "Sem Disciplinas", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "As disciplinas dos seus alunos podem ainda não ter sido publicadas.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "Houve um erro ao carregar as disciplinas dos alunos.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} Tarefa", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Classificado", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Submetido", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} pts", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Desculpado", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Em falta", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Cancelar", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Sim", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "Não", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Tentar novamente", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Eliminar", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Feito", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} em {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Termina {date} a {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_ru.arb b/libs/flutter_student_embed/lib/l10n/res/intl_ru.arb new file mode 100644 index 0000000000..4b80df0a1a --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_ru.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Курсы", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Календарь", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Календари", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Следующий месяц: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Предыдущий месяц: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Начало на следующей неделе {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Начало на предыдущей неделе {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Месяц из {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "развернуть", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "свернуть", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "можно получить {points} баллов", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} событие}other{{date}, {eventCount} события}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "На сегодня события отсутствуют!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "Похоже, что сегодня можно отдохнуть, расслабиться и набраться сил.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "Произошла ошибка при загрузке вашего календаря", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Выберите элементы для отображения в календаре.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Перейти к сегодня", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "Задачи", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Пока что нет описания", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Дата", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Редактировать", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "Новая задача", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Задача редактирования", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Заголовок", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Курс (не обязательно)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Нет", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Описание", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Сохранить", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Вы уверены?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Вы хотите удалить эту задачу?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Несохраненные изменения", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Вы действительно хотите закрыть эту страницу? Ваши несохраненные изменения будут потеряны.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Поле заголовка не может быть пустым", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "Произошла ошибка при сохранении этой задачи. Проверьте подключение и попробуйте еще раз.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "Произошла ошибка при удалении этой задачи. Проверьте подключение и попробуйте еще раз.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Ой-ой!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "Мы не знаем точно, что произошло, но это нехорошо. Обратитесь к нам, если это происходит дальше.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Просмотр подробностей ошибки", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Версия приложения", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Модель устройства", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Версия ОС Android", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Полное сообщение об ошибке", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "Курсы отсутствуют", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Курсы вашего студента пока еще не могут быть опубликованы.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "Произошла ошибка при загрузке календаря ваших учащихся.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "Задача {courseName}", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "С оценкой", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Отправлено", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} баллов", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "По уважительной причине", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Отсутствует", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Отменить", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Да", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "Нет", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Повторить", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Удалить", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Готово", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} в {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Срок выполнения {date} в {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_sl.arb b/libs/flutter_student_embed/lib/l10n/res/intl_sl.arb new file mode 100644 index 0000000000..5b1bb6c4d8 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_sl.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Predmeti", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Koledar", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Koledarji", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Naslednji mesec: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Predhodni mesec: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Naslednji teden z začetkom {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Predhodni teden z začetkom {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Mesec {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "razširi", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "strni", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} možnih točk", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} dogodek}other{{date}, {eventCount} dogodkov(-a/-i)}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "Danes ni dogodkov.", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "Videti je, da je to krasen dan za počitek, sprostitev in regeneracijo.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "Pri nalaganju vašega koledarja je prišlo do napake", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Izberite elemente za prikaz na koledarju.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Pojdi na danes", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "Čakajoča opravila", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Opisa še ni", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Datum", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Uredi", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "Nov seznam opravil", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Uredi seznam opravil", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Naslov", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Predmet (izbirni)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Brez", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Opis", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Shrani", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Ali ste prepričani?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Ali želite izbrisati ta element seznama opravil?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Neshranjene spremembe", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Ali ste prepričani, da želite zapreti to stran? Vaše neshranjene spremembe bodo izgubljene.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Polje z naslovom ne more ostati prazno", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "Prišlo je do napake pri shranjevanju tega seznama opravil. Preverite svojo povezavo in poskusite znova.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "Prišlo je do napake pri brisanju tega seznama opravil. Preverite svojo povezavo in poskusite znova.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Ojoj.", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "Nismo prepričano, kaj se je zgodilo, ni pa bilo dobro. Če se napaka ponovi, nas kontaktirajte.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Prikaz podrobnosti o napaki", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Različica aplikacije", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Model naprave", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Različica sistema Android", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Polni prikaz sporočila o napaki", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "Ni predmetov", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Predmeti vašega študenta morda še niso objavljeni.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "Pri nalaganju predmetov vašega študenta je prišlo do napake.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} Seznam opravil", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Ocenjeno", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Poslano", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} točk", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Opravičeno", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Manjkajoče", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Prekliči", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Da", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "Ne", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Ponovno poskusi", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Odstrani", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Dokončano", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} ob {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Roki {date} ob {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_sv.arb b/libs/flutter_student_embed/lib/l10n/res/intl_sv.arb new file mode 100644 index 0000000000..2ed3fb7586 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_sv.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Kurser", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Kalender", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Kalendrar", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Nästa månad: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Föregående månad: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Nästa vecka börjar {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Föregående vecka startade {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Månaden {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "visa", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "dölj", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} möjliga poäng", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} händelse}other{{date}, {eventCount} händelser}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "Inga händelser idag!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "Det verkar vara en bra dag för vila, avslappning och omladdning.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "Ett fel uppstod vid inläsning av din kalender", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Välj element som ska visas i kalendern.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Gå till i dag", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "Att göra", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Det finns ännu ingen beskrivning", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Datum", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Redigera", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "Ny att-göra", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Redigera att-göra", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Titel", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Kurs (valfri)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Ingen", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Beskrivning", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Spara", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Vill du fortsätta?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Vill du ta bort det här att-göra-objektet?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Osparade ändringar", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Är du säker på att du vill stänga den här sidan? Dina osparade ändringar kommer att gå förlorade.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Titeln får inte vara tom", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "Det gick inte att läsa in det här att-göra-objektet. Kontrollera din anslutning och försök igen.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "Det gick inte att ta bort det här att-göra-objektet. Kontrollera din anslutning och försök igen.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Oj då!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "Vi vet inte vad som hände, men det fungerar inte. Kontakta oss om detta fortsätter att inträffa.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Visa felinformation", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Programversion", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Enhetsmodell", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Android AS-version", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Fullständigt felmeddelande", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "Inga kurser", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Dina studentkurser kanske inte publicerats än.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "Det gick inte att läsa in din students kurser.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "Att-göra för {courseName}", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Har bedömts", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Skickad", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} poäng", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Ursäktad", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Saknad", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Avbryt", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Ja", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "Nej", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Försök igen", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Radera", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Klar", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} kl. {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Ska lämnas in {date} klockan {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_sv_instk12.arb b/libs/flutter_student_embed/lib/l10n/res/intl_sv_instk12.arb new file mode 100644 index 0000000000..bfccff3ee6 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_sv_instk12.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Kurser", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Kalender", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Kalendrar", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Nästa månad: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Föregående månad: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Nästa vecka börjar {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Föregående vecka startade {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Månaden {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "visa", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "dölj", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} möjliga poäng", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} händelse}other{{date}, {eventCount} händelser}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "Inga händelser idag!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "Det verkar vara en bra dag för vila, avslappning och omladdning.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "Ett fel uppstod vid inläsning av din kalender", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Välj element som ska visas i kalendern.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Gå till i dag", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "Att göra", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Det finns ännu ingen beskrivning", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Datum", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Redigera", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "Ny att-göra", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Redigera att-göra", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Titel", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Kurs (valfri)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Inga", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Beskrivning", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Spara", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Vill du fortsätta?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Vill du ta bort det här att-göra-objektet?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Osparade ändringar", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Är du säker på att du vill stänga den här sidan? Dina osparade ändringar kommer att gå förlorade.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Titeln får inte vara tom", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "Det gick inte att läsa in det här att-göra-objektet. Kontrollera din anslutning och försök igen.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "Det gick inte att ta bort det här att-göra-objektet. Kontrollera din anslutning och försök igen.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Oj då!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "Vi vet inte vad som hände, men det fungerar inte. Kontakta oss om detta fortsätter att inträffa.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Visa felinformation", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Programversion", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Enhetsmodell", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Android AS-version", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Fullständigt felmeddelande", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "Inga kurser", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Dina elevkurser kanske inte publicerats än.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "Det gick inte att läsa in din elevs kurser.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "Att-göra för {courseName}", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Har bedömts", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Inskickad", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} poäng", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Ursäktad", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Saknad", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Avbryt", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Ja", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "Nej", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Försök igen", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Ta bort", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Klar", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} kl. {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Ska lämnas in {date} klockan {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_th.arb b/libs/flutter_student_embed/lib/l10n/res/intl_th.arb new file mode 100644 index 0000000000..a30482afc3 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_th.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "บทเรียน", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "ปฏิทิน", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "ปฏิทิน", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "เดือนถัดไป: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "เดือนก่อนหน้า: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "สัปดาห์ถัดไป เริ่มต้น {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "สัปดาห์ก่อนหน้า เริ่มต้น {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "เดือน {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "ขยาย", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "ย่อ", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} คะแนนที่เป็นไปได้", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} กิจกรรม}other{{date}, {eventCount} กิจกรรม}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "ไม่มีกิจกรรมในวันนี้!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "ดูเหมือนนี่จะเป็นวันที่เหมาะสำหรับพัก ผ่อนคลายและเติมพลัง", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "มีข้อผิดพลาดในการโหลดปฏิทินของคุณ", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "เลือกองค์ประกอบที่จะจัดแสดงในปฏิทิน", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "ไปที่วันนี้", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "สิ่งที่ต้องทำ", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "ยังไม่มีรายละเอียด", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "วันที่", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "แก้ไข", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "รายการสิ่งที่ต้องทำใหม่", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "แก้ไขรายการสิ่งที่ต้องทำ", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "ชื่อ", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "บทเรียน (เผื่อเลือก)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "ไม่มี", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "รายละเอียด", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "บันทึก", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "แน่ใจหรือไม่", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "ต้องการลบรายการสิ่งที่ต้องทำนี้หรือไม่", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "การเปลี่ยนแปลงที่ไม่ได้บันทึก", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "แน่ใจว่าต้องการปิดหน้าเพจนี้หรือไม่ การเปลี่ยนแปลงของคุณที่ไม่ได้บันทึกไว้จะหายไป", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "ชื่อจะต้องไม่ปล่อยว่าง", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "มีข้อผิดพลาดในการบันทึกรายการสิ่งที่ต้องทำนี้ กรุณาตรวจสอบการเชื่อมต่อของคุณและลองใหม่อีกครั้ง", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "มีข้อผิดพลาดในการลบรายการสิ่งที่ต้องทำนี้ กรุณาตรวจสอบการเชื่อมต่อของคุณและลองใหม่อีกครั้ง", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "โอ๊ะ โอ!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "เราไม่แน่ใจว่าเกิดอะไรขึ้น แต่เชื่อว่าไม่ดี ติดต่อเราหากยังเกิดปัญหานี้อยู่", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "ดูรายละเอียดข้อผิดพลาด", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "เวอร์ชั่นแอพพลิเคชั่น", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "รุ่นอุปกรณ์", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "เวอร์ชั่น Android OS", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "ข้อความแจ้งข้อผิดพลาดเต็ม", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "ไม่มีบทเรียน", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "บทเรียนสำหรับผู้เรียนของคุณอาจยังไม่ได้เผยแพร่", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "มีข้อผิดพลาดในการโหลดบทเรียนสำหรับผู้เรียนของคุณ", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} สิ่งที่ต้องทำ", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "ให้เกรดแล้ว", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "จัดส่งแล้ว", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} คะแนน", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "ได้รับการยกเว้น", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "ขาดหาย", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "ยกเลิก", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "ใช่", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "ไม่", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "ลองใหม่", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "ลบ", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "เสร็จสิ้น", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} ที่ {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "ครบกำหนด {date} เมื่อ {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_vi.arb b/libs/flutter_student_embed/lib/l10n/res/intl_vi.arb new file mode 100644 index 0000000000..5d0e7f09c0 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_vi.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "Khóa Học", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "Lịch", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "Lịch", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "Tháng tiếp theo: {month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "Tháng trước: {month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "Tuần tiếp theo bắt đầu {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "Tuần trước bắt đầu {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "Tháng {month}", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "mở rộng", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "thu gọn", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "{points} điểm có thể đạt", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}, {eventCount} sự kiện}other{{date}, {eventCount} sự kiện}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "Không Có Sự Kiện Hôm Nay!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "Có vẻ là một ngày tuyệt vời để nghỉ ngơi, thư giãn và hồi sức.", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "Đã xảy ra lỗi khi tải lịch của bạn", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "Chọn các phần tử để hiển thị trên lịch.", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "Đi đến hôm nay", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "Việc Cần Làm", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "Chưa có mô tả", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "Ngày", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "Chỉnh Sửa", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "Việc Cần Làm Mới", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "Chỉnh Sửa Việc Cần Làm", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "Tiêu Đề", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "Khóa Học (không bắt buộc)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "Không Có", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "Mô Tả", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "Lưu", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "Bạn có chắc không?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "Bạn có muốn xóa mục Việc Cần Làm này không?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "Thay đổi chưa lưu", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "Bạn có chắc chắn muốn đóng trang này không? Thay đổi chưa lưu của bạn sẽ bị mất.", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "Không được để trống tiêu đề", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "Đã xảy ra lỗi lưu Việc Cần Làm này. Vui lòng kiểm tra kết nối của bạn rồi thử lại.", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "Đã xảy ra lỗi xóa Việc Cần Làm này. Vui lòng kiểm tra kết nối của bạn rồi thử lại.", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "Rất tiếc!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "Chúng tôi không chắc đã xảy ra vấn đề gì nhưng chắc chắn là không ổn. Hãy liên hệ chúng tôi nếu tình trạng này vẫn tiếp diễn.", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "Xem chi tiết lỗi", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "Phiên bản ứng dụng", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "Model thiết bị", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Phiên bản HĐH Android", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "Thông báo lỗi đầy đủ", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "Không Có Khóa Học", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "Khóa học của bạn có thể chưa được phát hành.", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "Đã xảy ra lỗi khi tải khóa học của sinh viên của bạn.", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} Việc Cần Làm", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "Đã Chấm Điểm", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "Đã Nộp", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} điểm", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "Đã Xin Phép", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "Bị Thiếu", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "Hủy", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "Có", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "Không", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "Thử Lại", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "Xóa", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "Đã xong", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date} vào {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "Đến hạn {date} vào {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_zh.arb b/libs/flutter_student_embed/lib/l10n/res/intl_zh.arb new file mode 100644 index 0000000000..7a31fbfe80 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_zh.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "课程", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "日历", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "日历", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "下个月:{month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "上个月:{month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "下周,从{date}开始", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "上周,从{date}开始", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "{month}月", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "展开", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "折叠", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "满分 {points}", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}、{eventCount} 个事件}other{{date}、{eventCount} 个事件}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "今天没有事件!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "今天是休息放松的一天。", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "加载日历时发生错误", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "请选择要在日历上显示的元素。", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "转至今天", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "待办事项", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "暂时没有描述", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "日期", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "编辑", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "新待办事项", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "编辑待办事项", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "标题", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "课程(可选)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "无", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "说明", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "保存", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "是否确定?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "是否要删除此待办事项?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "无法保存更改", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "是否确定要关闭此页面?您的未保存更改将丢失。", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "标题不能为空", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "保存此待办事项时发生错误。请检查您的连接,然后再试一次。", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "删除此待办事项时发生错误。请检查您的连接,然后再试一次。", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "嗳哟!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "不知道发生了什么事情,但似乎不太妙。如果问题持续,请联系我们。", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "查看错误详细信息", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "应用程序版本", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "设备型号", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Android操作系统版本", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "完整的错误消息", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "没有课程", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "学生的课程可能尚未发布。", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "加载学生的课程时发生错误。", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName}待办事项", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "已评分", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "已提交", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} 分", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "已免除", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "未交", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "取消", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "是", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "否", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "重试", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "删除", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "完成", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date},时间 {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "截止于 {date},{time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_zh_HK.arb b/libs/flutter_student_embed/lib/l10n/res/intl_zh_HK.arb new file mode 100644 index 0000000000..ea89647c0e --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_zh_HK.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "課程", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "行事曆", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "行事曆", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "下個月:{month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "上個月:{month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "下一週開始 {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "上一週開始 {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "{month} 的月份", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "展開", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "收起", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "可能 {points} 分", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}、{eventCount} 活動}other{{date}、{eventCount} 活動}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "今天並無活動!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "看來是適合休息、放鬆和充電的一天。", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "載入您的行事曆時發生錯誤", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "選擇要在行事曆上顯示的元素。", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "前往今天", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "待辦事項", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "尚未有描述說明", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "日期", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "編輯", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "新待辦事項", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "編輯待辦事項", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "標題", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "課程(可選)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "無", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "描述", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "儲存", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "您是否確定?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "是否要刪除此待辦事項?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "未儲存的變更", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "是否確定要關閉此頁面?您的未保存變更都將丟失。", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "不可留空標題", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "保存此待辦事項時出錯。請檢查您的連接然後重試。", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "刪除此待辦事項時出錯。請檢查您的連接然後重試。", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "噢!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "不確定發生什麼事,但不是好事。如果持續發生,請聯絡我們。", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "檢視錯誤詳細資料", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "應用程式版本", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "裝置機型", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Android 作業系統版本", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "完整錯誤訊息", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "無課程", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "您的學生的課程可能尚未發佈。", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "載入您的學生的課程時發生錯誤。", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} 待辦事項", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "已評分", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "已提交", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} 分", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "已免除", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "缺少", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "取消", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "是", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "否", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "重試", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "刪除", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "已完成", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "日期 {date},時間 {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "截止於 {date} 的 {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_zh_Hans.arb b/libs/flutter_student_embed/lib/l10n/res/intl_zh_Hans.arb new file mode 100644 index 0000000000..7a31fbfe80 --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_zh_Hans.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "课程", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "日历", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "日历", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "下个月:{month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "上个月:{month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "下周,从{date}开始", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "上周,从{date}开始", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "{month}月", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "展开", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "折叠", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "满分 {points}", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}、{eventCount} 个事件}other{{date}、{eventCount} 个事件}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "今天没有事件!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "今天是休息放松的一天。", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "加载日历时发生错误", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "请选择要在日历上显示的元素。", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "转至今天", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "待办事项", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "暂时没有描述", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "日期", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "编辑", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "新待办事项", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "编辑待办事项", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "标题", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "课程(可选)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "无", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "说明", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "保存", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "是否确定?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "是否要删除此待办事项?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "无法保存更改", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "是否确定要关闭此页面?您的未保存更改将丢失。", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "标题不能为空", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "保存此待办事项时发生错误。请检查您的连接,然后再试一次。", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "删除此待办事项时发生错误。请检查您的连接,然后再试一次。", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "嗳哟!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "不知道发生了什么事情,但似乎不太妙。如果问题持续,请联系我们。", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "查看错误详细信息", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "应用程序版本", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "设备型号", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Android操作系统版本", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "完整的错误消息", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "没有课程", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "学生的课程可能尚未发布。", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "加载学生的课程时发生错误。", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName}待办事项", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "已评分", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "已提交", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} 分", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "已免除", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "未交", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "取消", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "是", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "否", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "重试", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "删除", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "完成", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "{date},时间 {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "截止于 {date},{time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/flutter_student_embed/lib/l10n/res/intl_zh_Hant.arb b/libs/flutter_student_embed/lib/l10n/res/intl_zh_Hant.arb new file mode 100644 index 0000000000..ea89647c0e --- /dev/null +++ b/libs/flutter_student_embed/lib/l10n/res/intl_zh_Hant.arb @@ -0,0 +1,460 @@ +{ + "@@last_modified": "2023-03-31T11:03:46.341309", + "coursesLabel": "課程", + "@coursesLabel": { + "description": "The label for the Courses tab", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendar": "行事曆", + "@Calendar": { + "description": "Title of the calendar screen", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Calendars": "行事曆", + "@Calendars": { + "description": "Label for button that lets users select which calendars to display", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nextMonth": "下個月:{month}", + "@nextMonth": { + "description": "Label for the button that switches the calendar to the next month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "previousMonth": "上個月:{month}", + "@previousMonth": { + "description": "Label for the button that switches the calendar to the previous month", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "nextWeek": "下一週開始 {date}", + "@nextWeek": { + "description": "Label for the button that switches the calendar to the next week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "previousWeek": "上一週開始 {date}", + "@previousWeek": { + "description": "Label for the button that switches the calendar to the previous week", + "type": "text", + "placeholders_order": [ + "date" + ], + "placeholders": { + "date": {} + } + }, + "selectedMonthLabel": "{month} 的月份", + "@selectedMonthLabel": { + "description": "Accessibility label for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [ + "month" + ], + "placeholders": { + "month": {} + } + }, + "expand": "展開", + "@expand": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "收起", + "@collapse": { + "description": "Accessibility label for the on-tap hint for the button that expands/collapses the month view", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pointsPossible": "可能 {points} 分", + "@pointsPossible": { + "description": "Screen reader label used for the points possible for an assignment, quiz, etc.", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "calendarDaySemanticsLabel": "{eventCount,plural, =1{{date}、{eventCount} 活動}other{{date}、{eventCount} 活動}}", + "@calendarDaySemanticsLabel": { + "description": "Screen reader label used for calendar day, reads the date and count of events", + "type": "text", + "placeholders_order": [ + "date", + "eventCount" + ], + "placeholders": { + "date": {}, + "eventCount": {} + } + }, + "No Events Today!": "今天並無活動!", + "@No Events Today!": { + "description": "Title displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "It looks like a great day to rest, relax, and recharge.": "看來是適合休息、放鬆和充電的一天。", + "@It looks like a great day to rest, relax, and recharge.": { + "description": "Message displayed when there are no calendar events for the current day", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your calendar": "載入您的行事曆時發生錯誤", + "@There was an error loading your calendar": { + "description": "Message displayed when calendar events could not be loaded for the current student", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Select elements to display on the calendar.": "選擇要在行事曆上顯示的元素。", + "@Select elements to display on the calendar.": { + "description": "Select calendars description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Go to today": "前往今天", + "@Go to today": { + "description": "Accessibility label used for the today button in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "To Do": "待辦事項", + "@To Do": { + "description": "Label used for To-Do items in the planner", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There's no description yet": "尚未有描述說明", + "@There's no description yet": { + "description": "Message used when an item has no description", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Date": "日期", + "@Date": { + "description": "Label used for the date/time section", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit": "編輯", + "@Edit": { + "description": "Label for \"edit\" actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "New To Do": "新待辦事項", + "@New To Do": { + "description": "Title of the screen for creating new To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Edit To Do": "編輯待辦事項", + "@Edit To Do": { + "description": "Title of the screen for editing a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title": "標題", + "@Title": { + "description": "Hint shown for the title input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Course (optional)": "課程(可選)", + "@Course (optional)": { + "description": "Label for optional course selection when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "None": "無", + "@None": { + "description": "Label used when no course is selected when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Description": "描述", + "@Description": { + "description": "Hint shown for the description input when creating To Do items", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Save": "儲存", + "@Save": { + "description": "Label for the \"save\" action", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are You Sure?": "您是否確定?", + "@Are You Sure?": { + "description": "Title of the dialog shown when the user tries to perform an action that requires confirmation", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Do you want to delete this To Do item?": "是否要刪除此待辦事項?", + "@Do you want to delete this To Do item?": { + "description": "Message of the dialog shown when the user tries to delete a To Do item", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Unsaved changes": "未儲存的變更", + "@Unsaved changes": { + "description": "Title of the dialog shown when the user tries to leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Are you sure you wish to close this page? Your unsaved changes will be lost.": "是否確定要關閉此頁面?您的未保存變更都將丟失。", + "@Are you sure you wish to close this page? Your unsaved changes will be lost.": { + "description": "Body text of the dialog shown when the user tries leave with unsaved changes", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Title must not be empty": "不可留空標題", + "@Title must not be empty": { + "description": "Error message shown when the users attempts to save with an empty title", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error saving this To Do. Please check your connection and try again.": "保存此待辦事項時出錯。請檢查您的連接然後重試。", + "@There was an error saving this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error deleting this To Do. Please check your connection and try again.": "刪除此待辦事項時出錯。請檢查您的連接然後重試。", + "@There was an error deleting this To Do. Please check your connection and try again.": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Uh oh!": "噢!", + "@Uh oh!": { + "description": "Title of the screen that shows when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": "不確定發生什麼事,但不是好事。如果持續發生,請聯絡我們。", + "@We’re not sure what happened, but it wasn’t good. Contact us if this keeps happening.": { + "description": "Message shown when a crash has occurred", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "View error details": "檢視錯誤詳細資料", + "@View error details": { + "description": "Label for the button that allowed users to view crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Application version": "應用程式版本", + "@Application version": { + "description": "Label for the application version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Device model": "裝置機型", + "@Device model": { + "description": "Label for the device model displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Android OS version": "Android 作業系統版本", + "@Android OS version": { + "description": "Label for the Android operating system version displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Full error message": "完整錯誤訊息", + "@Full error message": { + "description": "Label for the full error message displayed in the crash details", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No Courses": "無課程", + "@No Courses": { + "description": "Title for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Your student’s courses might not be published yet.": "您的學生的課程可能尚未發佈。", + "@Your student’s courses might not be published yet.": { + "description": "Message for having no courses", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "There was an error loading your your student’s courses.": "載入您的學生的課程時發生錯誤。", + "@There was an error loading your your student’s courses.": { + "description": "Message displayed when the list of student courses could not be loaded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "courseToDo": "{courseName} 待辦事項", + "@courseToDo": { + "description": "Label used for course-specific To-Do items in the planner, where the course name is used as an adjective to describe the type of To Do", + "type": "text", + "placeholders_order": [ + "courseName" + ], + "placeholders": { + "courseName": {} + } + }, + "Graded": "已評分", + "@Graded": { + "description": "Label for assignments that have been graded", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Submitted": "已提交", + "@Submitted": { + "description": "Label for assignments that have been submitted", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "assignmentTotalPoints": "{points} 分", + "@assignmentTotalPoints": { + "description": "Label used for the total points the assignment is worth", + "type": "text", + "placeholders_order": [ + "points" + ], + "placeholders": { + "points": {} + } + }, + "Excused": "已免除", + "@Excused": { + "description": "Grading status for an assignment marked as excused", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Missing": "缺少", + "@Missing": { + "description": "Description for when a student has not turned anything in for an assignment", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Cancel": "取消", + "@Cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Yes": "是", + "@Yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "No": "否", + "@No": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Retry": "重試", + "@Retry": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Delete": "刪除", + "@Delete": { + "description": "Label used for general delete/remove actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Done": "已完成", + "@Done": { + "description": "Label for general done/finished actions", + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateAtTime": "日期 {date},時間 {time}", + "@dateAtTime": { + "description": "The string to format dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + }, + "dueDateAtTime": "截止於 {date} 的 {time}", + "@dueDateAtTime": { + "description": "The string to format due dates", + "type": "text", + "placeholders_order": [ + "date", + "time" + ], + "placeholders": { + "date": {}, + "time": {} + } + } +} \ No newline at end of file diff --git a/libs/pandares/src/main/res/values-hi/strings.xml b/libs/pandares/src/main/res/values-hi/strings.xml index ff5204c421..eadf730f30 100644 --- a/libs/pandares/src/main/res/values-hi/strings.xml +++ b/libs/pandares/src/main/res/values-hi/strings.xml @@ -68,6 +68,43 @@ लापता ग्रेड किए गए + अनुस्मारक + इस डिवाइस पर इस असाइनमेंट के बारे में नियत तिथि अनुस्मारक सूचनाएं जोड़ें। + अनुस्मारक जोड़ें + अनुस्मारक निकालें + %s से पहले + + 1 मिनट + %d मिनट + + + 1 घंटा + %d घंटे + + + 1 दिन + %d दिन + + + 1 सप्ताह + %d सप्ताह + + कस्टम + कस्टम अनुस्मारक + मात्रा + मिनट पहले + घंटे पहले + दिन पहले + सप्ताह पहले + अनुस्मारक सूचनाएं + असाइनमेंट अनुस्मारकों के लिए Canvas सूचनाएं। + नियत तिथि अनुस्मारक + यह असाइनमेंट %sमें नियत है: %s + कृपया अपने अनुस्मारक के लिए आगामी समय चुनें! + आपने पहले ही इस समय के लिए अनुस्मारक निर्धारित कर दिया है + अनुस्मारक मिटाएं + क्या आप वाकई इस अनुस्मारक को मिटाना चाहते हैं? + आपको इस कार्रवाई के लिए सटीक अलार्म अनुमति सक्षम करने की आवश्यकता है \'http://\' का उपयोग करने वाले URL के लिए कोई पूर्वावलोकन उपलब्ध नहीं है कृपया मान्य URL दर्ज करें @@ -1461,7 +1498,7 @@ ऑफ़लाइन सामग्री सिंक नहीं हो सकी सिंक रद्द करें? इस तरह से ऑफ़लाइन सिंक रुक जाएगा। आप इसे बाद में दोबारा कर सकते हैं। - एक या एक से ज़्यादा फ़ाइलें सिंक नहीं हो सकीं। अपना इंटरनेट कनेक्शन जांचें और सबमिट करने का पुनः प्रयास करें। + एक या अधिक आइटमें सिंक होने में विफल रहीं। कृपया अपने इंटरनेट कनेक्शन की जांच करें और फिर से सिंक करने की कोशिश करें। डाउनलोड शुरू हो रहा है पाठ्यक्रमों को ऑफ़लाइन रहते हुए पसंदीदा में नहीं जोड़ा जा सकता। सभी पाठ्यक्रम @@ -1490,4 +1527,76 @@ अतिरिक्त पाठ्यक्रम सामग्री डैशबोर्ड कार्ड ऑर्डर अपडेट करने में विफल + सभी मॉड्यूल और आइटमें प्रकाशित करें + केवल मॉड्यूल प्रकाशित करें + सभी मॉड्यूल और आइटमें अप्रकाशित करें + मॉड्यूल विकल्प + मॉड्यूल और सभी आइटमें प्रकाशित करें + केवल मॉड्यूल प्रकाशित करें + मॉड्यूल और सभी आइटमें अप्रकाशित करें + %sके लिए मॉड्यूल विकल्प + %s के लिए मॉड्यूल आइटम विकल्प + मॉड्यूल प्रकाशित करें + मॉड्यूल आइटम अप्रकाशित करें + प्रकाशित करें? + इस तरह से छात्रों को केवल मॉड्यूल ही दिखाई देगा। + इस तरह से मॉड्यूल और सभी आइटमें छात्रों को दिखाई देंगे। + अप्रकाशित करें? + इस तरह से मॉड्यूल और सभी आइटमें छात्रों को नहीं दिखाई देंगे। + इस तरह से छात्रों को केवल यह आइटम ही दिखाई देगी। + इस तरह से छात्रों को केवल यह आइटम ही दिखाई नहीं देगी। + इस तरह से सभी मॉड्यूल और आइटमें छात्रों को दिखाई देंगे। + इस तरह से छात्रों को केवल मॉड्यूल ही दिखाई देंगे। + इस तरह से सभी मॉड्यूल और आइटमें छात्रों को नहीं दिखाई देंगे। + केवल लिंक के साथ उपलब्ध है + शेड्यूल उपलब्धता + आइटम प्रकाशित की गई + आइटम अप्रकाशित की गई + केवल मॉड्यूल प्रकाशित + मॉड्यूल और सभी आइटमें प्रकाशित किए गए + मॉड्यूल और सभी आइटमें अप्रकाशित किए गए + केवल मॉड्यूल प्रकाशित + सभी मॉड्यूल और सभी आइटमें प्रकाशित किए गए + सभी मॉड्यूल और सभी आइटमें अप्रकाशित किए गए + पाठ्यक्रम से विरासत में पाएं + पाठ्यक्रम सदस्य + संस्थान सदस्य + सार्वजनिक + अनुमतियां संपादित करें + अपडेट करें + उपलब्धता + दृश्यता + इस तिथि से उपलब्ध: + इस तिथि तक उपलब्ध: + से + इस समय तक: + तिथि + समय + से तिथि साफ़ करें + तक तिथि साफ़ करें + इस प्रक्रिया में कुछ मिनट लग सकते हैं। आप इस प्रक्रिया के दौरान मोडल को बंद कर सकते हैं या पेज से दूर नेविगेट कर सकते हैं। + ध्यान दें + सभी मॉड्यूल्स + सभी मॉड्यूल और आइटमें + चयनित मॉड्यूल और आइटमें + चयनित मॉड्यूल + प्रकाशित किया जा रहा है + अप्रकाशित किया जा रहा है + जो मॉड्यूल और आइटम पहले ही संसाधित हो चुके हैं, प्रक्रिया बंद होने पर अपनी पूर्वस्थिति में वापस नहीं आएंगे। + काम सफल हुआ! + अपडेट करना विफल हुआ + अपडेट रद्द हुआ + छिपा हुआ + शेड्यूल किया गया + प्रकाशित + अप्रकाशित + + %.0f पॉइंट + %.0f पॉइंट्स + + प्रकाशित करें + अप्रकाशित करें + प्रकाशित करें + अप्रकाशित करें + रीफ़्रेश करें From 76d7fc4aced617139665a316ee868b3c4089d321 Mon Sep 17 00:00:00 2001 From: "kristof.nemere" Date: Mon, 29 Jul 2024 12:43:01 +0200 Subject: [PATCH 21/50] version bump --- apps/student/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/student/build.gradle b/apps/student/build.gradle index 296a57e8c6..b16fabed05 100644 --- a/apps/student/build.gradle +++ b/apps/student/build.gradle @@ -40,8 +40,8 @@ android { applicationId "com.instructure.candroid" minSdkVersion Versions.MIN_SDK targetSdkVersion Versions.TARGET_SDK - versionCode = 263 - versionName = '7.5.0' + versionCode = 264 + versionName = '7.5.1' vectorDrawables.useSupportLibrary = true multiDexEnabled = true From 846ba6fa0ec79623e09f85d8038a058d895d7e23 Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Tue, 30 Jul 2024 12:56:47 +0200 Subject: [PATCH 22/50] [MBL-17719][Teacher] Route to quiz details from quiz list (#2513) Test plan: See ticket. refs: MBL-17719 affects: Teacher release note: Fixed a bug, where opening a Quiz from the Quiz list screen would navigate to the wrong destination. --- .../instructure/teacher/fragments/QuizListFragment.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizListFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizListFragment.kt index 35305fe673..80311486d1 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizListFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/fragments/QuizListFragment.kt @@ -35,6 +35,8 @@ import com.instructure.teacher.adapters.QuizListAdapter import com.instructure.teacher.databinding.FragmentQuizListBinding import com.instructure.teacher.events.QuizUpdatedEvent import com.instructure.teacher.factory.QuizListPresenterFactory +import com.instructure.teacher.features.assignment.details.AssignmentDetailsFragment +import com.instructure.teacher.features.assignment.list.AssignmentListFragment import com.instructure.teacher.presenters.QuizListPresenter import com.instructure.teacher.router.RouteMatcher import com.instructure.teacher.utils.RecyclerViewUtils @@ -117,7 +119,13 @@ class QuizListFragment : BaseExpandableSyncFragment< override fun createAdapter(): QuizListAdapter { return QuizListAdapter(requireContext(), presenter, canvasContext.textAndIconColor) { quiz -> if (RouteMatcher.canRouteInternally(requireActivity(), quiz.htmlUrl, ApiPrefs.domain, false)) { - RouteMatcher.routeUrl(requireActivity(), quiz.htmlUrl!!, ApiPrefs.domain) + val route = RouteMatcher.getInternalRoute(quiz.htmlUrl!!, ApiPrefs.domain) + val secondaryClass = when (route?.primaryClass) { + QuizListFragment::class.java -> QuizDetailsFragment::class.java + AssignmentListFragment::class.java -> AssignmentDetailsFragment::class.java + else -> null + } + RouteMatcher.route(requireActivity(), route?.copy(secondaryClass = secondaryClass)) } else { val args = QuizDetailsFragment.makeBundle(quiz) RouteMatcher.route(requireActivity(), Route(null, QuizDetailsFragment::class.java, canvasContext, args)) From 11f92f57cbd4d1d7d8b4c629807b905ea1860033 Mon Sep 17 00:00:00 2001 From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com> Date: Tue, 30 Jul 2024 13:20:24 +0200 Subject: [PATCH 23/50] [MBL-17732][All] Making a conversation 'unread' by clicking on the toolbar 'unread' icon will not make the item disappear from 'archived' list #2512 refs: MBL-17732 affects: All release note: none --- .../features/inbox/list/InboxViewModel.kt | 8 ++++++-- .../features/inbox/list/InboxViewModelTest.kt | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/list/InboxViewModel.kt b/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/list/InboxViewModel.kt index 297400e204..c763de5e0a 100644 --- a/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/list/InboxViewModel.kt +++ b/libs/pandautils/src/main/java/com/instructure/pandautils/features/inbox/list/InboxViewModel.kt @@ -268,8 +268,12 @@ class InboxViewModel @Inject constructor( } fun markAsUnreadSelected() { - performBatchOperation("mark_as_unread") { ids, _ -> - updateItems(ids, unread = true) + performBatchOperation("mark_as_unread") { ids, progress -> + if (scope == InboxApi.Scope.ARCHIVED) { + removeItemsAndSilentUpdate(ids, progress) + } else { + updateItems(ids, unread = true) + } _events.value = Event(InboxAction.ShowConfirmationSnackbar(resources.getString(R.string.inboxMarkAsUnreadConfirmation, ids.size))) _events.value = Event(InboxAction.UpdateUnreadCount) } diff --git a/libs/pandautils/src/test/java/com/instructure/pandautils/features/inbox/list/InboxViewModelTest.kt b/libs/pandautils/src/test/java/com/instructure/pandautils/features/inbox/list/InboxViewModelTest.kt index 5615e04abc..0bc740bc52 100644 --- a/libs/pandautils/src/test/java/com/instructure/pandautils/features/inbox/list/InboxViewModelTest.kt +++ b/libs/pandautils/src/test/java/com/instructure/pandautils/features/inbox/list/InboxViewModelTest.kt @@ -320,6 +320,26 @@ class InboxViewModelTest { coVerify { inboxRepository.batchUpdateConversations(any(), eq("mark_as_unread")) } } + @Test + fun `Remove selected items from the list when marked as unread in archived scope`() { + coEvery { inboxRepository.getConversations(any(), any(), any(), any()) }.returnsMany( + DataResult.Success(listOf(Conversation(id = 1), Conversation(id = 2))), + DataResult.Success(listOf(Conversation(id = 1), Conversation(id = 2))), // We need an other call for the scope change + DataResult.Success(emptyList()) + ) + every { inboxEntryItemCreator.createInboxEntryItem(any(), any(), any(), any()) } answers { createItem(args[0] as Conversation, args[1], args[2], args[3], unread = true) } + + viewModel = createViewModel() + viewModel.data.observe(lifecycleOwner) {} + viewModel.scopeChanged(InboxApi.Scope.ARCHIVED) + viewModel.itemViewModels.value!![0].onLongClick(View(context)) + viewModel.itemViewModels.value!![1].onClick(View(context)) + viewModel.markAsUnreadSelected() + + assertTrue(viewModel.itemViewModels.value!!.isEmpty()) + coVerify { inboxRepository.batchUpdateConversations(any(), eq("mark_as_unread")) } + } + @Test fun `Mark selected items as read`() { coEvery { inboxRepository.getConversations(any(), any(), any(), any()) } returns DataResult.Success(listOf(Conversation(id = 1), Conversation(id = 2))) From 13bfd8e71de2b55ac7740510266c10f7e1d7bc04 Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Wed, 31 Jul 2024 13:00:50 +0200 Subject: [PATCH 24/50] [MBL-17611][Parent] AUP screen (#2514) Test plan: Test the AUP screen, should work like in the production version. The policy details cannot be opened yet, it depends on other tickets. refs: MBL-17611 affects: Parent release note: none --- .../instructure/parentapp/di/LoginModule.kt | 4 +- .../login/ParentAcceptableUsePolicyRouter.kt | 44 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 apps/parent/src/main/java/com/instructure/parentapp/features/login/ParentAcceptableUsePolicyRouter.kt diff --git a/apps/parent/src/main/java/com/instructure/parentapp/di/LoginModule.kt b/apps/parent/src/main/java/com/instructure/parentapp/di/LoginModule.kt index 0f31c84cb2..3edffaa553 100644 --- a/apps/parent/src/main/java/com/instructure/parentapp/di/LoginModule.kt +++ b/apps/parent/src/main/java/com/instructure/parentapp/di/LoginModule.kt @@ -20,6 +20,7 @@ package com.instructure.parentapp.di import androidx.fragment.app.FragmentActivity import com.instructure.loginapi.login.LoginNavigation import com.instructure.loginapi.login.features.acceptableusepolicy.AcceptableUsePolicyRouter +import com.instructure.parentapp.features.login.ParentAcceptableUsePolicyRouter import com.instructure.parentapp.features.login.ParentLoginNavigation import dagger.Module import dagger.Provides @@ -32,8 +33,7 @@ class LoginModule { @Provides fun provideAcceptableUsePolicyRouter(activity: FragmentActivity): AcceptableUsePolicyRouter { - // TODO: Implement - throw NotImplementedError() + return ParentAcceptableUsePolicyRouter(activity) } @Provides diff --git a/apps/parent/src/main/java/com/instructure/parentapp/features/login/ParentAcceptableUsePolicyRouter.kt b/apps/parent/src/main/java/com/instructure/parentapp/features/login/ParentAcceptableUsePolicyRouter.kt new file mode 100644 index 0000000000..81dcfc45fb --- /dev/null +++ b/apps/parent/src/main/java/com/instructure/parentapp/features/login/ParentAcceptableUsePolicyRouter.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ package com.instructure.parentapp.features.login + +import android.content.Intent +import android.webkit.CookieManager +import androidx.fragment.app.FragmentActivity +import com.instructure.loginapi.login.features.acceptableusepolicy.AcceptableUsePolicyRouter +import com.instructure.loginapi.login.tasks.LogoutTask +import com.instructure.parentapp.features.main.MainActivity +import com.instructure.parentapp.util.ParentLogoutTask + +class ParentAcceptableUsePolicyRouter( + private val activity: FragmentActivity +) : AcceptableUsePolicyRouter { + override fun openPolicy(content: String) { + TODO("Not yet implemented") + } + + override fun startApp() { + CookieManager.getInstance().flush() + + val intent = Intent(activity, MainActivity::class.java) + activity.intent?.extras?.let { intent.putExtras(it) } + activity.startActivity(intent) + } + + override fun logout() { + ParentLogoutTask(LogoutTask.Type.LOGOUT).execute() + } +} \ No newline at end of file From 4267a8fae53f61ab2152ab1aa26c90ee1e28c3a1 Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Wed, 31 Jul 2024 15:00:58 +0200 Subject: [PATCH 25/50] [MBL-17780][Student] - Add 'Grading Period' string resource and display it on the Assignment List Page (#2515) --- .../assignments/list/AssignmentListFragment.kt | 17 ++++++++++++++--- .../adapter/AssignmentListRecyclerAdapter.kt | 2 +- libs/pandares/src/main/res/values/strings.xml | 2 +- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/list/AssignmentListFragment.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/list/AssignmentListFragment.kt index 97c13658cc..cac1ea6e68 100644 --- a/apps/student/src/main/java/com/instructure/student/features/assignments/list/AssignmentListFragment.kt +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/list/AssignmentListFragment.kt @@ -44,7 +44,18 @@ import com.instructure.interactions.router.RouterParams import com.instructure.pandautils.analytics.SCREEN_VIEW_ASSIGNMENT_LIST import com.instructure.pandautils.analytics.ScreenView import com.instructure.pandautils.binding.viewBinding -import com.instructure.pandautils.utils.* +import com.instructure.pandautils.utils.Const +import com.instructure.pandautils.utils.ParcelableArg +import com.instructure.pandautils.utils.ViewStyler +import com.instructure.pandautils.utils.addSearch +import com.instructure.pandautils.utils.closeSearch +import com.instructure.pandautils.utils.isCourse +import com.instructure.pandautils.utils.isTablet +import com.instructure.pandautils.utils.makeBundle +import com.instructure.pandautils.utils.onClick +import com.instructure.pandautils.utils.setupAsBackButton +import com.instructure.pandautils.utils.toast +import com.instructure.pandautils.utils.withArgs import com.instructure.student.R import com.instructure.student.adapter.TermSpinnerAdapter import com.instructure.student.databinding.AssignmentListLayoutBinding @@ -93,7 +104,7 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { } private val allTermsGradingPeriod by lazy { - GradingPeriod().apply { title = getString(R.string.assignmentsListAllGradingPeriods) } + GradingPeriod().apply { title = getString(R.string.assignmentsListDisplayGradingPeriod) } } private val adapterToAssignmentsCallback = object : AdapterToAssignmentsCallback { @@ -291,7 +302,7 @@ class AssignmentListFragment : ParentFragment(), Bookmarkable { termSpinner.adapter = adapter termSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(adapterView: AdapterView<*>, view: View?, i: Int, l: Long) { - if (adapter.getItem(i)!!.title == getString(R.string.assignmentsListAllGradingPeriods)) { + if (adapter.getItem(i)!!.title == getString(R.string.assignmentsListDisplayGradingPeriod)) { recyclerAdapter?.loadAssignment() } else { recyclerAdapter?.loadAssignmentsForGradingPeriod(adapter.getItem(i)!!.id, true) diff --git a/apps/student/src/main/java/com/instructure/student/features/assignments/list/adapter/AssignmentListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/features/assignments/list/adapter/AssignmentListRecyclerAdapter.kt index 0dcb1d462a..fde92e41f2 100644 --- a/apps/student/src/main/java/com/instructure/student/features/assignments/list/adapter/AssignmentListRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/features/assignments/list/adapter/AssignmentListRecyclerAdapter.kt @@ -126,7 +126,7 @@ abstract class AssignmentListRecyclerAdapter( private fun loadAssignmentsData(course: Course) { //This check is for the "all grading periods" option if (currentGradingPeriod != null && currentGradingPeriod!!.title != null - && currentGradingPeriod!!.title == context.getString(R.string.assignmentsListAllGradingPeriods)) { + && currentGradingPeriod!!.title == context.getString(R.string.assignmentsListDisplayGradingPeriod)) { loadAssignment() return } diff --git a/libs/pandares/src/main/res/values/strings.xml b/libs/pandares/src/main/res/values/strings.xml index 93f8374e6e..5c2f17d87b 100644 --- a/libs/pandares/src/main/res/values/strings.xml +++ b/libs/pandares/src/main/res/values/strings.xml @@ -242,7 +242,7 @@ Sort by Time Sort by Type Cancel - All + Grading Period: All Sort assignments button, sort by time Sort assignments button, sort by type Assignments sorted by time From 00d4fa8a9965b3f69481d825f36eb0a0c3f3df8a Mon Sep 17 00:00:00 2001 From: Kristof Deak <92309696+kdeakinstructure@users.noreply.github.com> Date: Mon, 5 Aug 2024 13:21:52 +0200 Subject: [PATCH 26/50] [MBL-17387][Student] - Implement Offline Syllabus E2E Test (#2516) --- .../student/ui/e2e/SyllabusE2ETest.kt | 3 +- .../ui/e2e/offline/OfflineSyllabusE2ETest.kt | 112 ++++++++++++++++++ .../ui/interaction/SyllabusInteractionTest.kt | 15 +++ .../student/ui/pages/SyllabusPage.kt | 7 ++ 4 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyllabusE2ETest.kt diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt index 245e3c135d..06f3873648 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/SyllabusE2ETest.kt @@ -58,10 +58,11 @@ class SyllabusE2ETest: StudentTest() { dashboardPage.waitForRender() dashboardPage.selectCourse(course) - Log.d(STEP_TAG,"Navigate to Syllabus Page. Assert that the syllabus body string is displayed, and there are no tabs yet.") + Log.d(STEP_TAG,"Navigate to Syllabus Page. Assert that the syllabus body string is displayed, and there are no tabs yet, and the toolbar subtitle is the '${course.name}' course name.") courseBrowserPage.selectSyllabus() syllabusPage.assertNoTabs() syllabusPage.assertSyllabusBody("this is the syllabus body") + syllabusPage.assertToolbarCourseTitle(course.name) Log.d(PREPARATION_TAG,"Seed an assignment for '${course.name}' course.") val assignment = AssignmentsApi.createAssignment(course.id, teacher.token, submissionTypes = listOf(SubmissionType.ON_PAPER), withDescription = true, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyllabusE2ETest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyllabusE2ETest.kt new file mode 100644 index 0000000000..56278651f5 --- /dev/null +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/e2e/offline/OfflineSyllabusE2ETest.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2024 - present Instructure, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.instructure.student.ui.e2e.offline + +import android.util.Log +import com.google.android.material.checkbox.MaterialCheckBox +import com.instructure.canvas.espresso.FeatureCategory +import com.instructure.canvas.espresso.KnownBug +import com.instructure.canvas.espresso.OfflineE2E +import com.instructure.canvas.espresso.Priority +import com.instructure.canvas.espresso.SecondaryFeatureCategory +import com.instructure.canvas.espresso.TestCategory +import com.instructure.canvas.espresso.TestMetaData +import com.instructure.dataseeding.api.AssignmentsApi +import com.instructure.dataseeding.model.SubmissionType +import com.instructure.dataseeding.util.days +import com.instructure.dataseeding.util.fromNow +import com.instructure.dataseeding.util.iso8601 +import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.assertOfflineIndicator +import com.instructure.student.ui.e2e.offline.utils.OfflineTestUtils.waitForNetworkToGoOffline +import com.instructure.student.ui.utils.StudentComposeTest +import com.instructure.student.ui.utils.seedData +import com.instructure.student.ui.utils.tokenLogin +import dagger.hilt.android.testing.HiltAndroidTest +import org.junit.After +import org.junit.Test + +@HiltAndroidTest +class OfflineSyllabusE2ETest : StudentComposeTest() { + + override fun displaysPageObjects() = Unit + + override fun enableAndConfigureAccessibilityChecks() = Unit + + @OfflineE2E + @KnownBug(bugLink = "https://instructure.atlassian.net/browse/MBL-17787", explanation = "Syllabus tabs are not displaying in offline mode while only assignment summary type items exists.") + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.SYLLABUS, TestCategory.E2E, SecondaryFeatureCategory.OFFLINE_MODE) + fun testOfflineSyllabusE2E() { + + Log.d(PREPARATION_TAG,"Seeding data.") + val syllabusBody = "this is the syllabus body" + val data = seedData(students = 1, teachers = 1, courses = 1, syllabusBody = syllabusBody) + val student = data.studentsList[0] + val teacher = data.teachersList[0] + val course = data.coursesList[0] + + Log.d(PREPARATION_TAG,"Seed an assignment for '${course.name}' course.") + val assignment = AssignmentsApi.createAssignment(course.id, teacher.token, submissionTypes = listOf( + SubmissionType.ON_PAPER), withDescription = true, pointsPossible = 15.0, dueAt = 1.days.fromNow.iso8601) + + Log.d(STEP_TAG,"Login with user: '${student.name}', login id: '${student.loginId}'.") + tokenLogin(student) + dashboardPage.waitForRender() + + Log.d(STEP_TAG, "Open the '${course.name}' course's 'Manage Offline Content' page via the more menu of the Dashboard Page.") + dashboardPage.clickCourseOverflowMenu(course.name, "Manage Offline Content") + + Log.d(STEP_TAG, "Assert that the '${course.name}' course's checkbox state is 'Unchecked'.") + manageOfflineContentPage.assertCheckedStateOfItem(course.name, MaterialCheckBox.STATE_UNCHECKED) + + Log.d(STEP_TAG, "Expand the course. Select the 'Syllabus' of '${course.name}' course for sync. Click on the 'Sync' button.") + manageOfflineContentPage.expandCollapseItem(course.name) + manageOfflineContentPage.changeItemSelectionState("Syllabus") + manageOfflineContentPage.clickOnSyncButtonAndConfirm() + + Log.d(STEP_TAG, "Assert that the offline sync icon only displayed on the synced course's course card.") + dashboardPage.assertCourseOfflineSyncIconVisible(course.name) + device.waitForIdle() + + Log.d(PREPARATION_TAG, "Turn off the Wi-Fi and Mobile Data on the device, so it will go offline.") + turnOffConnectionViaADB() + waitForNetworkToGoOffline(device) + + Log.d(STEP_TAG,"Wait for the Dashboard Page to be rendered. Select '${course.name}' course.") + dashboardPage.waitForRender() + dashboardPage.selectCourse(course) + + Log.d(STEP_TAG,"Navigate to Syllabus Page. Assert that the syllabus body string is displayed. Assert that the toolbar subtitle is the '${course.name}' course name.") + courseBrowserPage.selectSyllabus() + syllabusPage.assertSyllabusBody(syllabusBody) + syllabusPage.assertToolbarCourseTitle(course.name) + + Log.d(STEP_TAG,"Navigate to 'Summary' tab. Assert that all of the items, so '${assignment.name}' assignment is displayed.") + syllabusPage.selectSummaryTab() + syllabusPage.assertItemDisplayed(assignment.name) + + Log.d(STEP_TAG, "Assert that the Offline Indicator (bottom banner) is displayed on the Page List Page.") + assertOfflineIndicator() + } + + @After + fun tearDown() { + Log.d(PREPARATION_TAG, "Turn back on the Wi-Fi and Mobile Data on the device, so it will come back online.") + turnOnConnectionViaADB() + } + +} \ No newline at end of file diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyllabusInteractionTest.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyllabusInteractionTest.kt index a9de867c12..723b246689 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyllabusInteractionTest.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/interaction/SyllabusInteractionTest.kt @@ -59,6 +59,21 @@ class SyllabusInteractionTest : StudentComposeTest() { calendarEventDetailsPage.verifyDescription(event.description!!) } + @Test + @TestMetaData(Priority.MANDATORY, FeatureCategory.SYLLABUS, TestCategory.INTERACTION) + fun testSyllabus_assignment() { + val data = goToSyllabus(eventCount = 0, assignmentCount = 1) + + val course = data.courses.values.first() + data.coursePermissions[course.id] = CanvasContextPermission(manageCalendar = true) + val assignment = data.assignments.entries.firstOrNull()!!.value + + syllabusPage.selectSummaryTab() + syllabusPage.assertItemDisplayed(assignment.name!!) + syllabusPage.selectSummaryEvent(assignment.name!!) + assignmentDetailsPage.assertAssignmentTitle(assignment.name!!) + } + private fun goToSyllabus(eventCount: Int, assignmentCount: Int) : MockCanvas { val data = MockCanvas.init(studentCount = 1, teacherCount = 1, courseCount = 1, favoriteCourseCount = 1) diff --git a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SyllabusPage.kt b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SyllabusPage.kt index df6acc3af8..00114048ff 100644 --- a/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SyllabusPage.kt +++ b/apps/student/src/androidTest/java/com/instructure/student/ui/pages/SyllabusPage.kt @@ -25,10 +25,12 @@ import androidx.test.espresso.web.sugar.Web import androidx.test.espresso.web.webdriver.DriverAtoms import androidx.test.espresso.web.webdriver.Locator import com.instructure.canvas.espresso.containsTextCaseInsensitive +import com.instructure.canvas.espresso.matchToolbarText import com.instructure.canvas.espresso.scrollRecyclerView import com.instructure.espresso.assertDisplayed import com.instructure.espresso.click import com.instructure.espresso.page.BasePage +import com.instructure.espresso.page.getStringFromResource import com.instructure.espresso.page.plus import com.instructure.espresso.page.withAncestor import com.instructure.espresso.swipeDown @@ -38,6 +40,11 @@ import org.hamcrest.Matchers.allOf open class SyllabusPage : BasePage(R.id.syllabusPage) { + fun assertToolbarCourseTitle(courseName: String) { + onView(withId(R.id.toolbar) + withAncestor(R.id.syllabusPage)).assertDisplayed().check(ViewAssertions.matches(matchToolbarText(Matchers.`is`(getStringFromResource(R.string.syllabus)), true))) + onView(withId(R.id.toolbar) + withAncestor(R.id.syllabusPage)).assertDisplayed().check(ViewAssertions.matches(matchToolbarText(Matchers.`is`(courseName), false))) + } + fun assertItemDisplayed(itemText: String) { scrollRecyclerView(R.id.syllabusEventsRecycler, itemText) } From b5444433a330955e28ab1c618c9c9ba4633a3e2a Mon Sep 17 00:00:00 2001 From: Tamas Kozmer <72397075+tamaskozmer@users.noreply.github.com> Date: Mon, 5 Aug 2024 17:19:08 +0200 Subject: [PATCH 27/50] [MBL-17776][Teacher] Assignments and discussions not opening from the Calendar on tablet #2520 refs: MBL-17776 affects: Teacher release note: Fixed an issue where assignments and discussions wouldn't open from the calendar on tablets. --- .../assignment/details/AssignmentDetailsFragment.kt | 2 -- .../teacher/features/calendar/TeacherCalendarRouter.kt | 6 ++++-- .../espresso/common/interaction/CalendarInteractionTest.kt | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/details/AssignmentDetailsFragment.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/details/AssignmentDetailsFragment.kt index 4efcd0bd57..41a7be5ecd 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/details/AssignmentDetailsFragment.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/assignment/details/AssignmentDetailsFragment.kt @@ -17,7 +17,6 @@ package com.instructure.teacher.features.assignment.details import android.os.Bundle import android.view.LayoutInflater -import android.webkit.WebChromeClient import android.webkit.WebView import com.instructure.canvasapi2.models.Assignment import com.instructure.canvasapi2.models.Assignment.Companion.getSubmissionTypeFromAPIString @@ -62,7 +61,6 @@ import com.instructure.teacher.events.AssignmentGradedEvent import com.instructure.teacher.events.AssignmentUpdatedEvent import com.instructure.teacher.events.post import com.instructure.teacher.factory.AssignmentDetailPresenterFactory -import com.instructure.teacher.features.assignment.submission.AssignmentSubmissionListPresenter import com.instructure.teacher.features.assignment.submission.AssignmentSubmissionListFragment import com.instructure.teacher.features.assignment.submission.SubmissionListFilter import com.instructure.teacher.fragments.DueDatesFragment diff --git a/apps/teacher/src/main/java/com/instructure/teacher/features/calendar/TeacherCalendarRouter.kt b/apps/teacher/src/main/java/com/instructure/teacher/features/calendar/TeacherCalendarRouter.kt index 88839b22d9..ca40764ff1 100644 --- a/apps/teacher/src/main/java/com/instructure/teacher/features/calendar/TeacherCalendarRouter.kt +++ b/apps/teacher/src/main/java/com/instructure/teacher/features/calendar/TeacherCalendarRouter.kt @@ -28,6 +28,8 @@ import com.instructure.pandautils.features.calendartodo.details.ToDoFragment import com.instructure.pandautils.features.discussion.router.DiscussionRouterFragment import com.instructure.teacher.activities.InitActivity import com.instructure.teacher.features.assignment.details.AssignmentDetailsFragment +import com.instructure.teacher.features.assignment.list.AssignmentListFragment +import com.instructure.teacher.fragments.DiscussionsListFragment import com.instructure.teacher.router.RouteMatcher class TeacherCalendarRouter(val activity: FragmentActivity) : CalendarRouter { @@ -36,11 +38,11 @@ class TeacherCalendarRouter(val activity: FragmentActivity) : CalendarRouter { } override fun openAssignment(canvasContext: CanvasContext, assignmentId: Long) { - RouteMatcher.route(activity, AssignmentDetailsFragment.makeRoute(canvasContext, assignmentId)) + RouteMatcher.route(activity, AssignmentDetailsFragment.makeRoute(canvasContext, assignmentId).copy(primaryClass = AssignmentListFragment::class.java)) } override fun openDiscussion(canvasContext: CanvasContext, discussionId: Long) { - val route = DiscussionRouterFragment.makeRoute(canvasContext, discussionId) + val route = DiscussionRouterFragment.makeRoute(canvasContext, discussionId).copy(primaryClass = DiscussionsListFragment::class.java) RouteMatcher.route(activity, route) } diff --git a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CalendarInteractionTest.kt b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CalendarInteractionTest.kt index 96e8d3ec56..5da8a48daf 100644 --- a/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CalendarInteractionTest.kt +++ b/automation/espresso/src/main/kotlin/com/instructure/canvas/espresso/common/interaction/CalendarInteractionTest.kt @@ -346,7 +346,6 @@ abstract class CalendarInteractionTest : CanvasComposeTest() { } @Test - @StubTablet("Known issue, see MBL-17776") fun selectDiscussionOpensDiscussionDetails() { val data = initData() From 92e450a1a55942cd4905a6232015fb4f81cd859b Mon Sep 17 00:00:00 2001 From: Akos Hermann <72087159+hermannakos@users.noreply.github.com> Date: Tue, 6 Aug 2024 13:24:42 +0200 Subject: [PATCH 28/50] [MBL-17781][Student] Handle cancellation on Discussion List (#2523) Test plan: See ticket. refs: MBL-17781 affects: Student release note: none --- .../discussion/list/adapter/DiscussionListRecyclerAdapter.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/student/src/main/java/com/instructure/student/features/discussion/list/adapter/DiscussionListRecyclerAdapter.kt b/apps/student/src/main/java/com/instructure/student/features/discussion/list/adapter/DiscussionListRecyclerAdapter.kt index 62ffa37c47..b8b792dc03 100644 --- a/apps/student/src/main/java/com/instructure/student/features/discussion/list/adapter/DiscussionListRecyclerAdapter.kt +++ b/apps/student/src/main/java/com/instructure/student/features/discussion/list/adapter/DiscussionListRecyclerAdapter.kt @@ -33,6 +33,7 @@ import com.instructure.student.features.discussion.list.DiscussionListRepository import com.instructure.student.holders.EmptyViewHolder import com.instructure.student.holders.NoViewholder import com.instructure.student.interfaces.AdapterToFragmentCallback +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import java.util.* @@ -107,6 +108,8 @@ open class DiscussionListRecyclerAdapter( try { discussions = repository.getDiscussionTopicHeaders(canvasContext, !isDiscussions, isRefresh) populateData() + } catch (e: CancellationException) { + callback.onRefreshFinished() } catch (e: Exception) { callback.onRefreshFinished() context.toast(R.string.errorOccurred) From c012bd99f8c3abf4612949186f9aa75218e346bc Mon Sep 17 00:00:00 2001 From: Kristof Nemere <109959688+kristofnemere@users.noreply.github.com> Date: Tue, 6 Aug 2024 14:29:54 +0200 Subject: [PATCH 29/50] [MBL-17748][Parent] Feedback button refs: MBL-17748 affects: Parent release note: Added submission feedback button to assignment details screen. * Parent feedback * Test fix * Handle download url * Fixed quizzes navigation * test fix --- .../lib/l10n/app_localizations.dart | 10 ++++ .../lib/network/utils/analytics.dart | 3 +- .../lib/router/panda_router.dart | 23 +++++++-- .../assignment_details_screen.dart | 39 +++++++++++++++ .../web_view/simple_web_view_screen.dart | 47 +++++++++++++++---- .../test/router/panda_router_test.dart | 19 ++++++-- .../assignment_details_screen_test.dart | 43 ++++++++++++++++- .../webview/simple_web_view_screen_test.dart | 6 +-- 8 files changed, 167 insertions(+), 23 deletions(-) diff --git a/apps/flutter_parent/lib/l10n/app_localizations.dart b/apps/flutter_parent/lib/l10n/app_localizations.dart index 86768767b3..b7f03e6be3 100644 --- a/apps/flutter_parent/lib/l10n/app_localizations.dart +++ b/apps/flutter_parent/lib/l10n/app_localizations.dart @@ -1715,4 +1715,14 @@ class AppLocalizations { String get needToEnablePermission => Intl.message('You need to enable exact alarm permission for this action', desc: 'Error message when the user tries to set a reminder without the permission'); + + String get submissionAndRubric => Intl.message( + 'Submission & Rubric', + desc: 'Button text for Submission and Rubric on Assignment Details Screen' + ); + + String get submission => Intl.message( + 'Submission', + desc: 'Title for WebView screen when opening submission' + ); } diff --git a/apps/flutter_parent/lib/network/utils/analytics.dart b/apps/flutter_parent/lib/network/utils/analytics.dart index 9254d431a0..c9eeb47f20 100644 --- a/apps/flutter_parent/lib/network/utils/analytics.dart +++ b/apps/flutter_parent/lib/network/utils/analytics.dart @@ -14,8 +14,8 @@ import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_parent/network/api/heap_api.dart'; -import 'package:flutter_parent/utils/features_utils.dart'; import 'package:flutter_parent/utils/debug_flags.dart'; +import 'package:flutter_parent/utils/features_utils.dart'; /// Event names /// The naming scheme for the majority of these is found in a google doc so that we can be consistent @@ -54,6 +54,7 @@ class AnalyticsEventConstants { static const USER_PROPERTY_BUILD_TYPE = 'build_type'; static const USER_PROPERTY_OS_VERSION = 'os_version'; static const VIEWED_OLD_REMINDER_MESSAGE = 'viewed_old_reminder_message'; + static const SUBMISSION_AND_RUBRIC_INTERACTION = 'submission_and_rubric_interaction'; } /// (Copied from canvas-api-2, make sure to stay in sync) diff --git a/apps/flutter_parent/lib/router/panda_router.dart b/apps/flutter_parent/lib/router/panda_router.dart index cf6f2d3d92..b4ea35148d 100644 --- a/apps/flutter_parent/lib/router/panda_router.dart +++ b/apps/flutter_parent/lib/router/panda_router.dart @@ -14,6 +14,7 @@ * along with this program. If not, see . */ +import 'dart:convert'; import 'dart:core'; import 'package:fluro/fluro.dart'; @@ -34,13 +35,13 @@ import 'package:flutter_parent/screens/dashboard/dashboard_screen.dart'; import 'package:flutter_parent/screens/domain_search/domain_search_screen.dart'; import 'package:flutter_parent/screens/events/event_details_screen.dart'; import 'package:flutter_parent/screens/help/help_screen.dart'; -import 'package:flutter_parent/screens/settings/legal_screen.dart'; import 'package:flutter_parent/screens/help/terms_of_use_screen.dart'; import 'package:flutter_parent/screens/inbox/conversation_list/conversation_list_screen.dart'; import 'package:flutter_parent/screens/login_landing_screen.dart'; import 'package:flutter_parent/screens/not_a_parent_screen.dart'; import 'package:flutter_parent/screens/pairing/qr_pairing_screen.dart'; import 'package:flutter_parent/screens/qr_login/qr_login_tutorial_screen.dart'; +import 'package:flutter_parent/screens/settings/legal_screen.dart'; import 'package:flutter_parent/screens/settings/settings_screen.dart'; import 'package:flutter_parent/screens/splash/splash_screen.dart'; import 'package:flutter_parent/screens/web_login/web_login_screen.dart'; @@ -152,8 +153,11 @@ class PandaRouter { static final String _simpleWebView = '/internal'; - static String simpleWebViewRoute(String url, String infoText) => - '/internal?${_RouterKeys.url}=${Uri.encodeQueryComponent(url)}&${_RouterKeys.infoText}=${Uri.encodeQueryComponent(infoText)}'; + static String simpleWebViewRoute(String url, String infoText, bool limitWebAccess) => + '/internal?${_RouterKeys.url}=${Uri.encodeQueryComponent(url)}&${_RouterKeys.infoText}=${Uri.encodeQueryComponent(infoText)}&${_RouterKeys.limitWebAccess}=${limitWebAccess}'; + + static String submissionWebViewRoute(String url, String title, Map cookies, bool limitWebAccess) => + '/internal?${_RouterKeys.url}=${Uri.encodeQueryComponent(url)}&${_RouterKeys.title}=${Uri.encodeQueryComponent(title)}&${_RouterKeys.cookies}=${jsonEncode(cookies)}&${_RouterKeys.limitWebAccess}=${limitWebAccess}'; static String settings() => '/settings'; @@ -376,7 +380,13 @@ class PandaRouter { static Handler _simpleWebViewHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { final url = params[_RouterKeys.url]![0]; final infoText = params[_RouterKeys.infoText]?.elementAt(0); - return SimpleWebViewScreen(url, url, infoText: infoText == null || infoText == 'null' ? null : infoText); + final titleParam = params[_RouterKeys.title]?.firstOrNull; + final title = (titleParam == null || titleParam.isEmpty) ? url : titleParam; + final cookiesParam = params[_RouterKeys.cookies]?.firstOrNull; + final cookies = (cookiesParam == null || cookiesParam.isEmpty) ? {} : jsonDecode(cookiesParam); + final limitWebAccess = params[_RouterKeys.limitWebAccess]?.firstOrNull == 'true'; + return SimpleWebViewScreen(url, title, limitWebAccess, + infoText: infoText == null || infoText == 'null' ? null : infoText, initialCookies: cookies); }); static Handler _syllabusHandler = Handler(handlerFunc: (BuildContext? context, Map> params) { @@ -461,7 +471,7 @@ class PandaRouter { final url = await _interactor.getAuthUrl(link); if (limitWebAccess) { // Special case for limit webview access flag (We don't want them to be able to navigate within the webview) - locator().pushRoute(context, simpleWebViewRoute(url, L10n(context).webAccessLimitedMessage)); + locator().pushRoute(context, simpleWebViewRoute(url, L10n(context).webAccessLimitedMessage, true)); } else if (await locator().canLaunch(link) ?? false) { // No native route found, let's launch the url if possible, or show an error toast locator().launch(url); @@ -519,6 +529,9 @@ class _RouterKeys { static final accountName = 'accountName'; static final eventId = 'eventId'; static final infoText = 'infoText'; + static final title = 'title'; + static final cookies = 'cookies'; + static final limitWebAccess = 'limitWebAccess'; static final isCreatingAccount = 'isCreatingAccount'; static final loginFlow = 'loginFlow'; static final qrLoginUrl = 'qrLoginUrl'; diff --git a/apps/flutter_parent/lib/screens/assignments/assignment_details_screen.dart b/apps/flutter_parent/lib/screens/assignments/assignment_details_screen.dart index 2814bad688..cf68adab8f 100644 --- a/apps/flutter_parent/lib/screens/assignments/assignment_details_screen.dart +++ b/apps/flutter_parent/lib/screens/assignments/assignment_details_screen.dart @@ -17,6 +17,7 @@ import 'package:flutter_parent/models/assignment.dart'; import 'package:flutter_parent/models/reminder.dart'; import 'package:flutter_parent/models/user.dart'; import 'package:flutter_parent/network/utils/api_prefs.dart'; +import 'package:flutter_parent/router/panda_router.dart'; import 'package:flutter_parent/screens/assignments/assignment_details_interactor.dart'; import 'package:flutter_parent/screens/assignments/grade_cell.dart'; import 'package:flutter_parent/screens/inbox/create_conversation/create_conversation_screen.dart'; @@ -33,6 +34,7 @@ import 'package:flutter_parent/utils/service_locator.dart'; import 'package:flutter_svg/svg.dart'; import 'package:permission_handler/permission_handler.dart'; +import '../../network/utils/analytics.dart'; import '../../utils/veneers/flutter_snackbar_veneer.dart'; class AssignmentDetailsScreen extends StatefulWidget { @@ -183,6 +185,30 @@ class _AssignmentDetailsScreenState extends State { ], ), ), + Divider(), + Padding( + padding: const EdgeInsets.only(top: 16.0, bottom: 16.0), + child: OutlinedButton( + onPressed: () { + _onSubmissionAndRubricClicked(assignment.htmlUrl, l10n.submission); + }, + child: Align( + alignment: Alignment.center, + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Text( + l10n.submissionAndRubric, + style: textTheme.titleMedium?.copyWith(color: ParentTheme.of(context)?.studentColor), + ), + SizedBox(width: 6), + Icon(CanvasIconsSolid.arrow_open_right, color: ParentTheme.of(context)?.studentColor, size: 14) + ])), + style: OutlinedButton.styleFrom( + minimumSize: Size(double.infinity, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6.0), + ), + side: BorderSide(width: 0.5, color: ParentTheme.of(context)?.onSurfaceColor ?? Colors.grey), + ))), if (!fullyLocked) ...[ Divider(), ..._rowTile( @@ -372,4 +398,17 @@ class _AssignmentDetailsScreenState extends State { locator.get().push(context, screen); } } + + _onSubmissionAndRubricClicked(String? assignmentUrl, String title) { + if (assignmentUrl == null) return; + final parentId = ApiPrefs.getUser()?.id ?? 0; + final currentStudentId = _currentStudent?.id ?? 0; + locator().pushRoute(context, PandaRouter.submissionWebViewRoute( + assignmentUrl, + title, + {"k5_observed_user_for_$parentId": "$currentStudentId"}, + false + )); + locator().logEvent(AnalyticsEventConstants.SUBMISSION_AND_RUBRIC_INTERACTION); + } } diff --git a/apps/flutter_parent/lib/utils/common_widgets/web_view/simple_web_view_screen.dart b/apps/flutter_parent/lib/utils/common_widgets/web_view/simple_web_view_screen.dart index cc186be9dc..dcb7c6ba10 100644 --- a/apps/flutter_parent/lib/utils/common_widgets/web_view/simple_web_view_screen.dart +++ b/apps/flutter_parent/lib/utils/common_widgets/web_view/simple_web_view_screen.dart @@ -19,12 +19,24 @@ import 'package:flutter_parent/utils/design/parent_theme.dart'; import 'package:flutter_parent/utils/web_view_utils.dart'; import 'package:webview_flutter/webview_flutter.dart'; +import '../../service_locator.dart'; +import '../../url_launcher.dart'; + class SimpleWebViewScreen extends StatefulWidget { - final String _url; - final String _title; - final String? _infoText; + final String url; + final String title; + final String? infoText; + final Map? initialCookies; + final bool limitWebAccess; - SimpleWebViewScreen(this._url, this._title, {String? infoText}) : _infoText = infoText; + SimpleWebViewScreen( + this.url, + this.title, + this.limitWebAccess, { + String? infoText, + Map? initialCookies, + }) : this.infoText = infoText, + this.initialCookies = initialCookies; @override State createState() => _SimpleWebViewScreenState(); @@ -43,7 +55,7 @@ class _SimpleWebViewScreenState extends State { backgroundColor: Colors.transparent, iconTheme: Theme.of(context).iconTheme, bottom: ParentTheme.of(context)?.appBarDivider(shadowInLightMode: false), - title: Text(widget._title, style: Theme.of(context).textTheme.titleLarge), + title: Text(widget.title, style: Theme.of(context).textTheme.titleLarge), ), body: WebView( javascriptMode: JavascriptMode.unrestricted, @@ -52,34 +64,51 @@ class _SimpleWebViewScreenState extends State { navigationDelegate: _handleNavigation, onWebViewCreated: (controller) { _controller = controller; - controller.loadUrl(widget._url); + controller.loadUrl(widget.url); }, onPageFinished: _handlePageLoaded, + initialCookies: _getCookies(), ), ), ); } NavigationDecision _handleNavigation(NavigationRequest request) { - if (!request.isForMainFrame || widget._url.startsWith(request.url)) return NavigationDecision.navigate; + if (request.url.contains('/download?download_frd=')) { + locator().launch(request.url); + return NavigationDecision.prevent; + } + if (!request.isForMainFrame || widget.url.startsWith(request.url) || !widget.limitWebAccess) return NavigationDecision.navigate; return NavigationDecision.prevent; } void _handlePageLoaded(String url) async { // If there's no info to show, just return - if (widget._infoText == null || widget._infoText!.isEmpty) return; + if (widget.infoText == null || widget.infoText!.isEmpty) return; // Run javascript to show the info alert await _controller?.evaluateJavascript(_showAlertJavascript); } + String _getDomain() { + final uri = Uri.parse(widget.url); + return uri.host; + } + + List _getCookies() { + return widget.initialCookies?.entries + .map((entry) => WebViewCookie(name: entry.key.toString(), value: entry.value.toString(), domain: _getDomain())) + .toList() ?? + []; + } + String get _showAlertJavascript => """ const floatNode = `