Signed-in users can view notifications (unread-only or all), paginated, and jump from an issue/PR notification to that repository's work items. No mark-read/unread/pinned write endpoint is exposed, matching the existing single-write-scope posture from ADR-004. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
243 lines
12 KiB
Kotlin
243 lines
12 KiB
Kotlin
package com.ubisam.ubgitea
|
|
|
|
import android.content.ActivityNotFoundException
|
|
import android.content.Intent
|
|
import android.net.Uri
|
|
import android.os.Bundle
|
|
import androidx.activity.ComponentActivity
|
|
import androidx.activity.compose.setContent
|
|
import androidx.activity.enableEdgeToEdge
|
|
import androidx.activity.viewModels
|
|
import androidx.compose.runtime.LaunchedEffect
|
|
import androidx.compose.runtime.collectAsState
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.runtime.mutableStateOf
|
|
import androidx.compose.runtime.setValue
|
|
import androidx.compose.runtime.saveable.rememberSaveable
|
|
import com.ubisam.ubgitea.domain.model.RepositoryCoordinates
|
|
import com.ubisam.ubgitea.domain.model.RepositoryRef
|
|
import com.ubisam.ubgitea.feature.codebrowser.CodeBrowserScreen
|
|
import com.ubisam.ubgitea.feature.codebrowser.CodeBrowserTarget
|
|
import com.ubisam.ubgitea.feature.codebrowser.CodeBrowserViewModel
|
|
import com.ubisam.ubgitea.feature.codebrowser.CodeReferenceKind
|
|
import com.ubisam.ubgitea.feature.history.HistoryScreen
|
|
import com.ubisam.ubgitea.feature.history.HistoryTarget
|
|
import com.ubisam.ubgitea.feature.history.HistoryViewModel
|
|
import com.ubisam.ubgitea.feature.notifications.NotificationsScreen
|
|
import com.ubisam.ubgitea.feature.notifications.NotificationsViewModel
|
|
import com.ubisam.ubgitea.feature.repositories.ReferenceKind
|
|
import com.ubisam.ubgitea.feature.repositories.RepositoryListScreen
|
|
import com.ubisam.ubgitea.feature.repositories.RepositoryListViewModel
|
|
import com.ubisam.ubgitea.feature.workitems.WorkItemsScreen
|
|
import com.ubisam.ubgitea.feature.workitems.WorkItemsTarget
|
|
import com.ubisam.ubgitea.feature.workitems.WorkItemsViewModel
|
|
import com.ubisam.ubgitea.ui.navigation.Route
|
|
import com.ubisam.ubgitea.ui.navigation.RouteStackSaver
|
|
import com.ubisam.ubgitea.ui.theme.UBGiteaTheme
|
|
|
|
class MainActivity : ComponentActivity() {
|
|
private val viewModel: RepositoryListViewModel by viewModels {
|
|
RepositoryListViewModel.Factory(application)
|
|
}
|
|
private val codeBrowserViewModel: CodeBrowserViewModel by viewModels {
|
|
CodeBrowserViewModel.Factory(application)
|
|
}
|
|
private val historyViewModel: HistoryViewModel by viewModels {
|
|
HistoryViewModel.Factory(application)
|
|
}
|
|
private val workItemsViewModel: WorkItemsViewModel by viewModels {
|
|
WorkItemsViewModel.Factory(application)
|
|
}
|
|
private val notificationsViewModel: NotificationsViewModel by viewModels {
|
|
NotificationsViewModel.Factory(application)
|
|
}
|
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
enableEdgeToEdge()
|
|
setContent {
|
|
val state by viewModel.state.collectAsState()
|
|
val codeBrowserState by codeBrowserViewModel.state.collectAsState()
|
|
val historyState by historyViewModel.state.collectAsState()
|
|
val workItemsState by workItemsViewModel.state.collectAsState()
|
|
val notificationsState by notificationsViewModel.state.collectAsState()
|
|
|
|
var routeStack by rememberSaveable(stateSaver = RouteStackSaver) {
|
|
mutableStateOf(listOf<Route>(Route.Repositories))
|
|
}
|
|
fun push(route: Route) { routeStack = routeStack + route }
|
|
fun popToRepositories() { routeStack = listOf(Route.Repositories) }
|
|
fun pop() { routeStack = routeStack.dropLast(1).ifEmpty { listOf(Route.Repositories) } }
|
|
val currentRoute = routeStack.last()
|
|
|
|
LaunchedEffect(currentRoute) {
|
|
when (val route = currentRoute) {
|
|
Route.Repositories -> Unit // RepositoryListViewModel restores its own session on creation.
|
|
is Route.CodeBrowser -> codeBrowserViewModel.open(
|
|
CodeBrowserTarget(
|
|
repository = RepositoryCoordinates.of(route.owner, route.name),
|
|
ref = RepositoryRef.of(route.ref),
|
|
kind = CodeReferenceKind.valueOf(route.refKind),
|
|
),
|
|
)
|
|
is Route.History -> historyViewModel.open(
|
|
HistoryTarget(RepositoryCoordinates.of(route.owner, route.name), RepositoryRef.of(route.ref)),
|
|
)
|
|
is Route.WorkItems -> workItemsViewModel.open(
|
|
WorkItemsTarget(RepositoryCoordinates.of(route.owner, route.name)),
|
|
)
|
|
Route.Notifications -> notificationsViewModel.open()
|
|
}
|
|
}
|
|
|
|
fun authenticationExpired() {
|
|
codeBrowserViewModel.close()
|
|
historyViewModel.close()
|
|
workItemsViewModel.close()
|
|
notificationsViewModel.close()
|
|
popToRepositories()
|
|
viewModel.logout()
|
|
}
|
|
|
|
UBGiteaTheme {
|
|
when (currentRoute) {
|
|
Route.Notifications -> NotificationsScreen(
|
|
state = notificationsState,
|
|
onShowUnreadOnly = notificationsViewModel::showUnreadOnly,
|
|
onShowAll = notificationsViewModel::showAll,
|
|
onNotificationSelected = { notification ->
|
|
push(Route.WorkItems(notification.repositoryOwner, notification.repositoryName))
|
|
},
|
|
onNextPage = notificationsViewModel::nextPage,
|
|
onPreviousPage = notificationsViewModel::previousPage,
|
|
onRetry = notificationsViewModel::retry,
|
|
onBack = {
|
|
notificationsViewModel.close()
|
|
pop()
|
|
},
|
|
onAuthenticationExpired = ::authenticationExpired,
|
|
)
|
|
is Route.WorkItems -> WorkItemsScreen(
|
|
state = workItemsState,
|
|
onShowIssues = workItemsViewModel::showIssues,
|
|
onShowPullRequests = workItemsViewModel::showPullRequests,
|
|
onIssueSelected = workItemsViewModel::openIssue,
|
|
onPullRequestSelected = workItemsViewModel::openPullRequest,
|
|
onNextPage = workItemsViewModel::nextPage,
|
|
onPreviousPage = workItemsViewModel::previousPage,
|
|
onRetryList = workItemsViewModel::retryList,
|
|
onRetryDetail = workItemsViewModel::retryDetail,
|
|
onBackToList = workItemsViewModel::backToList,
|
|
onBack = {
|
|
workItemsViewModel.close()
|
|
pop()
|
|
},
|
|
onAuthenticationExpired = ::authenticationExpired,
|
|
)
|
|
is Route.History -> HistoryScreen(
|
|
state = historyState,
|
|
onCommitSelected = historyViewModel::openCommit,
|
|
onNextPage = historyViewModel::nextPage,
|
|
onPreviousPage = historyViewModel::previousPage,
|
|
onRetryList = historyViewModel::retryList,
|
|
onRetryDetail = historyViewModel::retryDetail,
|
|
onRetryPatch = historyViewModel::retryPatch,
|
|
onBackToList = historyViewModel::backToList,
|
|
onBack = {
|
|
historyViewModel.close()
|
|
pop()
|
|
},
|
|
onCompare = historyViewModel::compareAgainstCurrentRef,
|
|
onCompareNextPage = historyViewModel::compareNextPage,
|
|
onComparePreviousPage = historyViewModel::comparePreviousPage,
|
|
onRetryCompare = historyViewModel::retryCompare,
|
|
onLoadComparePatch = historyViewModel::loadComparePatch,
|
|
onAuthenticationExpired = ::authenticationExpired,
|
|
)
|
|
is Route.CodeBrowser -> CodeBrowserScreen(
|
|
state = codeBrowserState,
|
|
onEntrySelected = codeBrowserViewModel::openEntry,
|
|
onBreadcrumbSelected = codeBrowserViewModel::openBreadcrumb,
|
|
onRetry = codeBrowserViewModel::retry,
|
|
onBack = {
|
|
if (!codeBrowserViewModel.navigateUp()) {
|
|
codeBrowserViewModel.close()
|
|
pop()
|
|
}
|
|
},
|
|
onAuthenticationExpired = ::authenticationExpired,
|
|
onViewHistory = { target ->
|
|
push(Route.History(target.repository.owner, target.repository.name, target.ref.value))
|
|
},
|
|
)
|
|
Route.Repositories -> RepositoryListScreen(
|
|
state = state,
|
|
onSignIn = viewModel::signIn,
|
|
onCancelSignIn = viewModel::cancelSignIn,
|
|
onRetry = viewModel::retry,
|
|
onLogout = viewModel::logout,
|
|
onRepositorySelected = viewModel::selectRepository,
|
|
onReferenceSelected = { repository, name, kind ->
|
|
viewModel.selectReference(name, kind)
|
|
push(
|
|
Route.CodeBrowser(
|
|
owner = repository.coordinates.owner,
|
|
name = repository.coordinates.name,
|
|
refKind = if (kind == ReferenceKind.BRANCH) {
|
|
CodeReferenceKind.BRANCH.name
|
|
} else {
|
|
CodeReferenceKind.TAG.name
|
|
},
|
|
ref = name,
|
|
),
|
|
)
|
|
},
|
|
onReferencesRefresh = viewModel::refreshReferences,
|
|
onBackToRepositories = viewModel::backToRepositories,
|
|
onNextPage = viewModel::nextPage,
|
|
onPreviousPage = viewModel::previousPage,
|
|
onRefreshRepositories = viewModel::refreshRepositories,
|
|
onRetryLastPushedBranch = viewModel::retryRowLastPushedBranch,
|
|
onShowAllRepositories = viewModel::showAllRepositories,
|
|
onShowFavorites = viewModel::showFavorites,
|
|
onToggleFavorite = viewModel::toggleFavorite,
|
|
onViewWorkItems = { repository ->
|
|
push(Route.WorkItems(repository.coordinates.owner, repository.coordinates.name))
|
|
},
|
|
onSelectOrganization = viewModel::selectOrganization,
|
|
onViewNotifications = { push(Route.Notifications) },
|
|
)
|
|
}
|
|
LaunchedEffect(viewModel) {
|
|
viewModel.browserRequests.collect(::openSystemBrowser)
|
|
}
|
|
}
|
|
}
|
|
consumeCallback(intent)
|
|
}
|
|
|
|
override fun onNewIntent(intent: Intent) {
|
|
super.onNewIntent(intent)
|
|
setIntent(intent)
|
|
consumeCallback(intent)
|
|
}
|
|
|
|
private fun openSystemBrowser(uri: String) {
|
|
try {
|
|
startActivity(
|
|
Intent(Intent.ACTION_VIEW, Uri.parse(uri)).apply {
|
|
addCategory(Intent.CATEGORY_BROWSABLE)
|
|
},
|
|
)
|
|
} catch (_: ActivityNotFoundException) {
|
|
viewModel.browserLaunchFailed()
|
|
}
|
|
}
|
|
|
|
private fun consumeCallback(intent: Intent?) {
|
|
val callback = intent?.dataString ?: return
|
|
setIntent(Intent(intent).setData(null))
|
|
viewModel.handleCallback(callback)
|
|
}
|
|
}
|