1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

Middleware | Integrate 0.9.0 (#417)

This commit is contained in:
Evgenii Kozlov 2020-05-13 19:41:37 +02:00 committed by GitHub
parent 045a02141d
commit ad9b4905d1
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
76 changed files with 3281 additions and 1921 deletions

View file

@ -1,5 +1,22 @@
# Change log for Android @Anytype app.
## Version 0.0.31
### New features 🚀
### Fixes & tech 🚒
* Migrate from short name emojis to unicode when parsing document icons (#408)
* Consuming event payload from middleware callaback responses (#408)
* Hard-coded alpha invite code for internal use (#408)
* `PageViewModel` refactoring (#408)
### Middleware ⚙️
* Updated middleware to `0.9.0` (#339)
## Version 0.0.30
### New features 🚀

View file

@ -94,7 +94,7 @@ class ComponentManager(private val main: MainComponent) {
.build()
}
val pageComponent = Component {
val pageComponent = ComponentMap {
main
.pageComponentBuilder()
.pageModule(PageModule())
@ -141,4 +141,17 @@ class ComponentManager(private val main: MainComponent) {
instance = null
}
}
class ComponentMap<T>(private val builder: () -> T) {
private val map = mutableMapOf<String, T>()
fun get(id: String) = map[id] ?: builder().also { map[id] = it }
fun new(id: String) = builder().also { map[id] = it }
fun release(id: String) {
map.remove(id)
}
}
}

View file

@ -13,6 +13,7 @@ import com.agileburo.anytype.domain.event.interactor.InterceptEvents
import com.agileburo.anytype.domain.image.ImageLoader
import com.agileburo.anytype.domain.image.LoadImage
import com.agileburo.anytype.domain.page.CreatePage
import com.agileburo.anytype.presentation.desktop.HomeDashboardEventConverter
import com.agileburo.anytype.presentation.desktop.HomeDashboardViewModelFactory
import com.agileburo.anytype.ui.desktop.HomeDashboardFragment
import dagger.Module
@ -49,7 +50,8 @@ class HomeDashboardModule {
closeDashboard: CloseDashboard,
getConfig: GetConfig,
dnd: DragAndDrop,
interceptEvents: InterceptEvents
interceptEvents: InterceptEvents,
eventConverter: HomeDashboardEventConverter
): HomeDashboardViewModelFactory = HomeDashboardViewModelFactory(
getCurrentAccount = getCurrentAccount,
loadImage = loadImage,
@ -58,7 +60,8 @@ class HomeDashboardModule {
closeDashboard = closeDashboard,
getConfig = getConfig,
dnd = dnd,
interceptEvents = interceptEvents
interceptEvents = interceptEvents,
eventConverter = eventConverter
)
@Provides
@ -126,4 +129,10 @@ class HomeDashboardModule {
context = Dispatchers.IO,
channel = channel
)
@Provides
@PerScreen
fun provideEventConverter(): HomeDashboardEventConverter {
return HomeDashboardEventConverter.DefaultConverter()
}
}

View file

@ -7,13 +7,15 @@ import com.agileburo.anytype.domain.block.interactor.*
import com.agileburo.anytype.domain.block.repo.BlockRepository
import com.agileburo.anytype.domain.download.DownloadFile
import com.agileburo.anytype.domain.download.Downloader
import com.agileburo.anytype.domain.emoji.Emojifier
import com.agileburo.anytype.domain.event.interactor.EventChannel
import com.agileburo.anytype.domain.event.interactor.InterceptEvents
import com.agileburo.anytype.domain.misc.UrlBuilder
import com.agileburo.anytype.domain.page.*
import com.agileburo.anytype.presentation.page.DocumentExternalEventReducer
import com.agileburo.anytype.presentation.page.Editor
import com.agileburo.anytype.presentation.page.PageViewModelFactory
import com.agileburo.anytype.presentation.page.editor.Interactor
import com.agileburo.anytype.presentation.page.editor.Orchestrator
import com.agileburo.anytype.presentation.page.render.DefaultBlockViewRenderer
import com.agileburo.anytype.presentation.page.selection.SelectionStateHolder
import com.agileburo.anytype.presentation.page.toggle.ToggleStateHolder
@ -44,67 +46,31 @@ class PageModule {
fun providePageViewModelFactory(
openPage: OpenPage,
closePage: ClosePage,
undo: Undo,
redo: Redo,
updateText: UpdateText,
createBlock: CreateBlock,
interceptEvents: InterceptEvents,
updateCheckbox: UpdateCheckbox,
unlinkBlocks: UnlinkBlocks,
updateLinkMarks: UpdateLinkMarks,
removeLinkMark: RemoveLinkMark,
duplicateBlock: DuplicateBlock,
updateTextStyle: UpdateTextStyle,
updateTextColor: UpdateTextColor,
updateBackgroundColor: UpdateBackgroundColor,
mergeBlocks: MergeBlocks,
splitBlock: SplitBlock,
createPage: CreatePage,
createDocument: CreateDocument,
uploadUrl: UploadUrl,
documentExternalEventReducer: DocumentExternalEventReducer,
urlBuilder: UrlBuilder,
downloadFile: DownloadFile,
renderer: DefaultBlockViewRenderer,
counter: Counter,
archiveDocument: ArchiveDocument,
replaceBlock: ReplaceBlock,
patternMatcher: DefaultPatternMatcher,
updateTitle: UpdateTitle,
selectionStateHolder: SelectionStateHolder,
updateAlignment: UpdateBlockAlignment
interactor: Orchestrator
): PageViewModelFactory = PageViewModelFactory(
openPage = openPage,
closePage = closePage,
createPage = createPage,
createDocument = createDocument,
updateText = updateText,
createBlock = createBlock,
interceptEvents = interceptEvents,
updateCheckbox = updateCheckbox,
unlinkBlocks = unlinkBlocks,
duplicateBlock = duplicateBlock,
updateTextStyle = updateTextStyle,
updateTextColor = updateTextColor,
updateBackgroundColor = updateBackgroundColor,
updateLinkMarks = updateLinkMarks,
removeLinkMark = removeLinkMark,
mergeBlocks = mergeBlocks,
uploadUrl = uploadUrl,
splitBlock = splitBlock,
documentEventReducer = documentExternalEventReducer,
urlBuilder = urlBuilder,
downloadFile = downloadFile,
renderer = renderer,
counter = counter,
undo = undo,
redo = redo,
archiveDocument = archiveDocument,
replaceBlock = replaceBlock,
patternMatcher = patternMatcher,
updateTitle = updateTitle,
selectionStateHolder = selectionStateHolder,
updateAlignment = updateAlignment
interactor = interactor
)
@Provides
@ -220,14 +186,6 @@ class PageModule {
repo = repo
)
@Provides
@PerScreen
fun provideUpdateAlignmentUseCase(
repo: BlockRepository
): UpdateBlockAlignment = UpdateBlockAlignment(
repo = repo
)
@Provides
@PerScreen
fun provideUpdateBackgroundColorUseCase(
@ -256,13 +214,13 @@ class PageModule {
@Provides
@PerScreen
fun provideDefaultBlockViewRenderer(
emojifier: Emojifier,
urlBuilder: UrlBuilder,
toggleStateHolder: ToggleStateHolder
toggleStateHolder: ToggleStateHolder,
counter: Counter
): DefaultBlockViewRenderer = DefaultBlockViewRenderer(
urlBuilder = urlBuilder,
emojifier = emojifier,
toggleStateHolder = toggleStateHolder
toggleStateHolder = toggleStateHolder,
counter = counter
)
@Provides
@ -333,4 +291,86 @@ class PageModule {
@Provides
@PerScreen
fun provideSelectionStateHolder(): SelectionStateHolder = SelectionStateHolder.Default()
@Provides
@PerScreen
fun provideMemory(
selectionStateHolder: SelectionStateHolder
): Editor.Memory = Editor.Memory(
selections = selectionStateHolder
)
@Provides
@PerScreen
fun provideInteractor(
storage: Editor.Storage,
proxer: Editor.Proxer,
memory: Editor.Memory,
createBlock: CreateBlock,
replaceBlock: ReplaceBlock,
duplicateBlock: DuplicateBlock,
updateTextColor: UpdateTextColor,
updateBackgroundColor: UpdateBackgroundColor,
splitBlock: SplitBlock,
mergeBlocks: MergeBlocks,
unlinkBlocks: UnlinkBlocks,
updateTextStyle: UpdateTextStyle,
updateCheckbox: UpdateCheckbox,
downloadFile: DownloadFile,
updateTitle: UpdateTitle,
updateText: UpdateText,
updateAlignment: UpdateAlignment,
textInteractor: Interactor.TextInteractor,
undo: Undo,
redo: Redo
): Orchestrator = Orchestrator(
stores = storage,
createBlock = createBlock,
replaceBlock = replaceBlock,
proxies = proxer,
duplicateBlock = duplicateBlock,
updateBackgroundColor = updateBackgroundColor,
updateTextColor = updateTextColor,
splitBlock = splitBlock,
mergeBlocks = mergeBlocks,
unlinkBlocks = unlinkBlocks,
undo = undo,
redo = redo,
updateTextStyle = updateTextStyle,
updateCheckbox = updateCheckbox,
memory = memory,
downloadFile = downloadFile,
updateTitle = updateTitle,
textInteractor = textInteractor,
updateText = updateText,
updateAlignment = updateAlignment
)
@Provides
@PerScreen
fun provideProxer(): Editor.Proxer = Editor.Proxer()
@Provides
@PerScreen
fun provideStorage(): Editor.Storage = Editor.Storage()
@Provides
@PerScreen
fun provideTextInteractor(
proxer: Editor.Proxer,
storage: Editor.Storage,
matcher: DefaultPatternMatcher
): Interactor.TextInteractor = Interactor.TextInteractor(
proxies = proxer,
stores = storage,
matcher = matcher
)
@Provides
@PerScreen
fun provideUpdateAlignmentUseCase(
repo: BlockRepository
): UpdateAlignment = UpdateAlignment(
repo = repo
)
}

View file

@ -108,7 +108,7 @@ class HomeDashboardFragment : ViewStateFragment<State>(R.layout.fragment_desktop
state.dashboard?.let { dashboard ->
lifecycleScope.launch {
val result = withContext(Dispatchers.IO) {
dashboard.toView(emojifier = emojifier)
dashboard.toView()
}
dashboardAdapter.update(result)
}

View file

@ -45,6 +45,8 @@ import com.agileburo.anytype.domain.ext.getSubstring
import com.agileburo.anytype.ext.extractMarks
import com.agileburo.anytype.presentation.page.PageViewModel
import com.agileburo.anytype.presentation.page.PageViewModelFactory
import com.agileburo.anytype.presentation.page.editor.Command
import com.agileburo.anytype.presentation.page.editor.ViewState
import com.agileburo.anytype.ui.base.NavigationFragment
import com.agileburo.anytype.ui.page.modals.*
import com.agileburo.anytype.ui.page.modals.actions.BlockActionToolbarFactory
@ -472,42 +474,42 @@ open class PageFragment :
}
}
private fun execute(event: EventWrapper<PageViewModel.Command>) {
private fun execute(event: EventWrapper<Command>) {
event.getContentIfNotHandled()?.let { command ->
when (command) {
is PageViewModel.Command.OpenPagePicker -> {
is Command.OpenPagePicker -> {
PageIconPickerFragment.newInstance(
context = requireArguments().getString(ID_KEY, ID_EMPTY_VALUE),
target = command.target
).show(childFragmentManager, null)
}
is PageViewModel.Command.OpenAddBlockPanel -> {
is Command.OpenAddBlockPanel -> {
AddBlockFragment.newInstance().show(childFragmentManager, null)
}
is PageViewModel.Command.OpenTurnIntoPanel -> {
is Command.OpenTurnIntoPanel -> {
TurnIntoFragment.single(
target = command.target
).show(childFragmentManager, null)
}
is PageViewModel.Command.OpenMultiSelectTurnIntoPanel -> {
is Command.OpenMultiSelectTurnIntoPanel -> {
TurnIntoFragment.multiple().show(childFragmentManager, null)
}
is PageViewModel.Command.OpenBookmarkSetter -> {
is Command.OpenBookmarkSetter -> {
CreateBookmarkFragment.newInstance(
context = command.context,
target = command.target
).show(childFragmentManager, null)
}
is PageViewModel.Command.OpenGallery -> {
is Command.OpenGallery -> {
openGalleryWithPermissionCheck(command.mediaType)
}
is PageViewModel.Command.RequestDownloadPermission -> {
is Command.RequestDownloadPermission -> {
startDownloadWithPermissionCheck(command.id)
}
is PageViewModel.Command.PopBackStack -> {
is Command.PopBackStack -> {
childFragmentManager.popBackStack()
}
is PageViewModel.Command.OpenActionBar -> {
is Command.OpenActionBar -> {
hideKeyboard()
childFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.action_bar_enter, R.anim.action_bar_exit)
@ -515,10 +517,10 @@ open class PageFragment :
.addToBackStack(null)
.commit()
}
is PageViewModel.Command.CloseKeyboard -> {
is Command.CloseKeyboard -> {
hideSoftInput()
}
is PageViewModel.Command.Browse -> {
is Command.Browse -> {
try {
Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(command.url)
@ -533,13 +535,13 @@ open class PageFragment :
}
}
private fun render(state: PageViewModel.ViewState) {
private fun render(state: ViewState) {
when (state) {
is PageViewModel.ViewState.Success -> {
is ViewState.Success -> {
pageAdapter.updateWithDiffUtil(state.blocks)
resetDocumentTitle(state)
}
is PageViewModel.ViewState.OpenLinkScreen -> {
is ViewState.OpenLinkScreen -> {
SetLinkFragment.newInstance(
blockId = state.block.id,
initUrl = state.block.getFirstLinkMarkupParam(state.range),
@ -548,11 +550,11 @@ open class PageFragment :
rangeStart = state.range.first
).show(childFragmentManager, null)
}
is PageViewModel.ViewState.Error -> toast(state.message)
is ViewState.Error -> toast(state.message)
}
}
private fun resetDocumentTitle(state: PageViewModel.ViewState.Success) {
private fun resetDocumentTitle(state: ViewState.Success) {
state.blocks.firstOrNull { view ->
view is BlockView.Title
}?.let { view ->
@ -648,11 +650,11 @@ open class PageFragment :
}
override fun injectDependencies() {
componentManager().pageComponent.get().inject(this)
componentManager().pageComponent.get(extractDocumentId()).inject(this)
}
override fun releaseDependencies() {
componentManager().pageComponent.release()
componentManager().pageComponent.release(extractDocumentId())
}
companion object {

View file

@ -21,8 +21,4 @@ abstract class BaseFragment(
super.onDestroy()
if (fragmentScope) releaseDependencies()
}
companion object {
const val CORE_COMPONENT_PROVIDER_ERROR = "Activity should implement core component provider"
}
}

View file

@ -9,6 +9,7 @@ import com.agileburo.anytype.domain.block.model.Command
import com.agileburo.anytype.domain.block.model.Position
import com.agileburo.anytype.domain.config.Config
import com.agileburo.anytype.domain.event.model.Event
import com.agileburo.anytype.domain.event.model.Payload
fun AccountEntity.toDomain(): Account {
return Account(
@ -88,6 +89,7 @@ fun BlockEntity.Content.toDomain(): Block.Content = when (this) {
is BlockEntity.Content.File -> toDomain()
is BlockEntity.Content.Icon -> toDomain()
is BlockEntity.Content.Bookmark -> toDomain()
is BlockEntity.Content.Smart -> toDomain()
}
fun BlockEntity.Content.File.toDomain(): Block.Content.File {
@ -185,6 +187,11 @@ fun BlockEntity.Content.Image.toDomain(): Block.Content.Image {
fun BlockEntity.Content.Divider.toDomain() = Block.Content.Divider
fun BlockEntity.Content.Smart.toDomain() = Block.Content.Smart(
type = Block.Content.Smart.Type.valueOf(type.name)
)
fun Block.Content.Image.toEntity(): BlockEntity.Content.Image {
return BlockEntity.Content.Image(
path = path
@ -219,6 +226,7 @@ fun Block.Content.toEntity(): BlockEntity.Content = when (this) {
is Block.Content.File -> toEntity()
is Block.Content.Icon -> toEntity()
is Block.Content.Bookmark -> toEntity()
is Block.Content.Smart -> toEntity()
}
fun Block.Content.File.toEntity(): BlockEntity.Content.File {
@ -444,6 +452,11 @@ fun Position.toEntity(): PositionEntity {
return PositionEntity.valueOf(name)
}
fun PayloadEntity.toDomain(): Payload = Payload(
context = context,
events = events.map { it.toDomain() }
)
fun EventEntity.toDomain(): Event {
return when (this) {
is EventEntity.Command.ShowBlock -> {

View file

@ -14,6 +14,12 @@ data class BlockEntity(
sealed class Content {
data class Smart(
val type: Type
) : Content() {
enum class Type { HOME, PAGE, ARCHIVE, BREADCRUMBS, PROFILE }
}
data class Text(
val text: String,
val style: Style,
@ -47,7 +53,7 @@ data class BlockEntity(
}
data class Layout(val type: Type) : Content() {
enum class Type { ROW, COLUMN }
enum class Type { ROW, COLUMN, DIV }
}
data class Image(

View file

@ -0,0 +1,6 @@
package com.agileburo.anytype.data.auth.model
data class PayloadEntity(
val context: String,
val events: List<EventEntity>
)

View file

@ -5,6 +5,7 @@ import com.agileburo.anytype.data.auth.mapper.toEntity
import com.agileburo.anytype.domain.block.model.Command
import com.agileburo.anytype.domain.block.repo.BlockRepository
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.domain.event.model.Payload
class BlockDataRepository(
private val factory: BlockDataStoreFactory
@ -12,24 +13,23 @@ class BlockDataRepository(
override suspend fun getConfig() = factory.remote.getConfig().toDomain()
override suspend fun openDashboard(contextId: String, id: String) {
factory.remote.openDashboard(id = id, contextId = contextId)
}
override suspend fun openDashboard(
contextId: String,
id: String
) = factory.remote.openDashboard(id = id, contextId = contextId).toDomain()
override suspend fun openPage(id: String): Payload = factory.remote.openPage(id).toDomain()
override suspend fun closeDashboard(id: String) {
factory.remote.closeDashboard(id)
}
override suspend fun updateAlignment(command: Command.UpdateAlignment) {
factory.remote.updateAlignment(command.toEntity())
}
override suspend fun updateAlignment(
command: Command.UpdateAlignment
) : Payload = factory.remote.updateAlignment(command.toEntity()).toDomain()
override suspend fun createPage(parentId: String) = factory.remote.createPage(parentId)
override suspend fun openPage(id: String) {
factory.remote.openPage(id)
}
override suspend fun closePage(id: String) {
factory.remote.closePage(id)
}
@ -46,19 +46,35 @@ class BlockDataRepository(
factory.remote.updateTextStyle(command.toEntity())
}
override suspend fun updateTextColor(command: Command.UpdateTextColor) {
factory.remote.updateTextColor(command.toEntity())
}
override suspend fun updateTextColor(
command: Command.UpdateTextColor
): Payload = factory.remote.updateTextColor(command.toEntity()).toDomain()
override suspend fun updateBackgroundColor(command: Command.UpdateBackgroundColor) {
factory.remote.updateBackroundColor(command.toEntity())
}
override suspend fun updateBackgroundColor(
command: Command.UpdateBackgroundColor
): Payload = factory.remote.updateBackroundColor(command.toEntity()).toDomain()
override suspend fun updateCheckbox(command: Command.UpdateCheckbox) {
factory.remote.updateCheckbox(command.toEntity())
}
override suspend fun create(command: Command.Create) = factory.remote.create(command.toEntity())
override suspend fun create(command: Command.Create): Pair<Id, Payload> {
return factory.remote.create(command.toEntity()).let { (id, payload) ->
Pair(id, payload.toDomain())
}
}
override suspend fun replace(
command: Command.Replace
): Pair<Id, Payload> = factory.remote.replace(command.toEntity()).let { (id, payload) ->
Pair(id, payload.toDomain())
}
override suspend fun duplicate(
command: Command.Duplicate
): Pair<Id, Payload> = factory.remote.duplicate(command.toEntity()).let { (id, payload) ->
Pair(id, payload.toDomain())
}
override suspend fun createDocument(
command: Command.CreateDocument
@ -68,19 +84,20 @@ class BlockDataRepository(
factory.remote.dnd(command.toEntity())
}
override suspend fun duplicate(command: Command.Duplicate) =
factory.remote.duplicate(command.toEntity())
override suspend fun unlink(
command: Command.Unlink
): Payload = factory.remote.unlink(command.toEntity()).toDomain()
override suspend fun unlink(command: Command.Unlink) {
factory.remote.unlink(command.toEntity())
override suspend fun merge(
command: Command.Merge
): Payload = factory.remote.merge(command.toEntity()).toDomain()
override suspend fun split(
command: Command.Split
): Pair<Id, Payload> = factory.remote.split(command.toEntity()).let { (id, payload) ->
Pair(id, payload.toDomain())
}
override suspend fun merge(command: Command.Merge) {
factory.remote.merge(command.toEntity())
}
override suspend fun split(command: Command.Split) = factory.remote.split(command.toEntity())
override suspend fun setIconName(
command: Command.SetIconName
) = factory.remote.setIconName(command.toEntity())
@ -100,8 +117,4 @@ class BlockDataRepository(
override suspend fun archiveDocument(
command: Command.ArchiveDocument
) = factory.remote.archiveDocument(command.toEntity())
override suspend fun replace(
command: Command.Replace
): Id = factory.remote.replace(command.toEntity())
}

View file

@ -2,34 +2,39 @@ package com.agileburo.anytype.data.auth.repo.block
import com.agileburo.anytype.data.auth.model.CommandEntity
import com.agileburo.anytype.data.auth.model.ConfigEntity
import com.agileburo.anytype.data.auth.model.PayloadEntity
import com.agileburo.anytype.domain.common.Id
interface BlockDataStore {
suspend fun create(command: CommandEntity.Create): Id
suspend fun create(command: CommandEntity.Create): Pair<Id, PayloadEntity>
suspend fun replace(command: CommandEntity.Replace): Pair<Id, PayloadEntity>
suspend fun duplicate(command: CommandEntity.Duplicate): Pair<Id, PayloadEntity>
suspend fun split(command: CommandEntity.Split): Pair<Id, PayloadEntity>
suspend fun merge(command: CommandEntity.Merge): PayloadEntity
suspend fun updateTextColor(command: CommandEntity.UpdateTextColor): PayloadEntity
suspend fun updateBackroundColor(command: CommandEntity.UpdateBackgroundColor): PayloadEntity
suspend fun updateAlignment(command: CommandEntity.UpdateAlignment) : PayloadEntity
suspend fun openDashboard(contextId: String, id: String): PayloadEntity
suspend fun createDocument(command: CommandEntity.CreateDocument): Pair<Id, Id>
suspend fun updateDocumentTitle(command: CommandEntity.UpdateTitle)
suspend fun updateText(command: CommandEntity.UpdateText)
suspend fun updateTextStyle(command: CommandEntity.UpdateStyle)
suspend fun updateTextColor(command: CommandEntity.UpdateTextColor)
suspend fun updateBackroundColor(command: CommandEntity.UpdateBackgroundColor)
suspend fun updateAlignment(command: CommandEntity.UpdateAlignment)
suspend fun updateCheckbox(command: CommandEntity.UpdateCheckbox)
suspend fun uploadUrl(command: CommandEntity.UploadBlock)
suspend fun dnd(command: CommandEntity.Dnd)
suspend fun duplicate(command: CommandEntity.Duplicate): Id
suspend fun merge(command: CommandEntity.Merge)
suspend fun split(command: CommandEntity.Split): Id
suspend fun unlink(command: CommandEntity.Unlink)
suspend fun unlink(command: CommandEntity.Unlink): PayloadEntity
suspend fun getConfig(): ConfigEntity
suspend fun createPage(parentId: String): String
suspend fun openPage(id: String)
suspend fun openPage(id: String): PayloadEntity
suspend fun closePage(id: String)
suspend fun openDashboard(contextId: String, id: String)
suspend fun closeDashboard(id: String)
suspend fun setIconName(command: CommandEntity.SetIconName)
suspend fun setupBookmark(command: CommandEntity.SetupBookmark)
suspend fun undo(command: CommandEntity.Undo)
suspend fun redo(command: CommandEntity.Redo)
suspend fun archiveDocument(command: CommandEntity.ArchiveDocument)
suspend fun replace(command: CommandEntity.Replace): Id
}

View file

@ -2,28 +2,34 @@ package com.agileburo.anytype.data.auth.repo.block
import com.agileburo.anytype.data.auth.model.CommandEntity
import com.agileburo.anytype.data.auth.model.ConfigEntity
import com.agileburo.anytype.data.auth.model.PayloadEntity
import com.agileburo.anytype.domain.common.Id
interface BlockRemote {
suspend fun create(command: CommandEntity.Create): Id
suspend fun create(command: CommandEntity.Create): Pair<String, PayloadEntity>
suspend fun replace(command: CommandEntity.Replace): Pair<String, PayloadEntity>
suspend fun duplicate(command: CommandEntity.Duplicate): Pair<String, PayloadEntity>
suspend fun split(command: CommandEntity.Split): Pair<Id, PayloadEntity>
suspend fun merge(command: CommandEntity.Merge): PayloadEntity
suspend fun unlink(command: CommandEntity.Unlink): PayloadEntity
suspend fun updateTextColor(command: CommandEntity.UpdateTextColor): PayloadEntity
suspend fun updateBackgroundColor(command: CommandEntity.UpdateBackgroundColor): PayloadEntity
suspend fun updateAlignment(command: CommandEntity.UpdateAlignment) : PayloadEntity
suspend fun createDocument(command: CommandEntity.CreateDocument): Pair<String, String>
suspend fun updateDocumentTitle(command: CommandEntity.UpdateTitle)
suspend fun updateText(command: CommandEntity.UpdateText)
suspend fun updateTextStyle(command: CommandEntity.UpdateStyle)
suspend fun updateTextColor(command: CommandEntity.UpdateTextColor)
suspend fun updateBackgroundColor(command: CommandEntity.UpdateBackgroundColor)
suspend fun updateAlignment(command: CommandEntity.UpdateAlignment)
suspend fun updateCheckbox(command: CommandEntity.UpdateCheckbox)
suspend fun dnd(command: CommandEntity.Dnd)
suspend fun merge(command: CommandEntity.Merge)
suspend fun split(command: CommandEntity.Split): Id
suspend fun duplicate(command: CommandEntity.Duplicate): Id
suspend fun unlink(command: CommandEntity.Unlink)
suspend fun getConfig(): ConfigEntity
suspend fun createPage(parentId: String): String
suspend fun openPage(id: String)
suspend fun openPage(id: String): PayloadEntity
suspend fun closePage(id: String)
suspend fun openDashboard(contextId: String, id: String)
suspend fun openDashboard(contextId: String, id: String): PayloadEntity
suspend fun closeDashboard(id: String)
suspend fun setIconName(command: CommandEntity.SetIconName)
suspend fun uploadUrl(command: CommandEntity.UploadBlock)
@ -31,5 +37,4 @@ interface BlockRemote {
suspend fun undo(command: CommandEntity.Undo)
suspend fun redo(command: CommandEntity.Redo)
suspend fun archiveDocument(command: CommandEntity.ArchiveDocument)
suspend fun replace(command: CommandEntity.Replace): Id
}

View file

@ -1,24 +1,24 @@
package com.agileburo.anytype.data.auth.repo.block
import com.agileburo.anytype.data.auth.model.CommandEntity
import com.agileburo.anytype.data.auth.model.PayloadEntity
import com.agileburo.anytype.domain.common.Id
class BlockRemoteDataStore(private val remote: BlockRemote) : BlockDataStore {
override suspend fun getConfig() = remote.getConfig()
override suspend fun openDashboard(contextId: String, id: String) {
remote.openDashboard(id = id, contextId = contextId)
}
override suspend fun openDashboard(
contextId: String,
id: String
) = remote.openDashboard(id = id, contextId = contextId)
override suspend fun closeDashboard(id: String) {
remote.closeDashboard(id = id)
}
override suspend fun createPage(parentId: String): String = remote.createPage(parentId)
override suspend fun openPage(id: String) {
remote.openPage(id)
}
override suspend fun openPage(id: String): PayloadEntity = remote.openPage(id)
override suspend fun closePage(id: String) {
remote.closePage(id)
@ -36,27 +36,29 @@ class BlockRemoteDataStore(private val remote: BlockRemote) : BlockDataStore {
remote.updateTextStyle(command)
}
override suspend fun updateTextColor(command: CommandEntity.UpdateTextColor) {
remote.updateTextColor(command)
}
override suspend fun updateTextColor(
command: CommandEntity.UpdateTextColor
): PayloadEntity = remote.updateTextColor(command)
override suspend fun updateBackroundColor(command: CommandEntity.UpdateBackgroundColor) {
remote.updateBackgroundColor(command)
}
override suspend fun updateBackroundColor(
command: CommandEntity.UpdateBackgroundColor
): PayloadEntity = remote.updateBackgroundColor(command)
override suspend fun updateCheckbox(command: CommandEntity.UpdateCheckbox) {
remote.updateCheckbox(command)
}
override suspend fun updateAlignment(command: CommandEntity.UpdateAlignment) {
remote.updateAlignment(command)
}
override suspend fun updateAlignment(
command: CommandEntity.UpdateAlignment
) : PayloadEntity = remote.updateAlignment(command)
override suspend fun uploadUrl(command: CommandEntity.UploadBlock) {
remote.uploadUrl(command)
}
override suspend fun create(command: CommandEntity.Create): String = remote.create(command)
override suspend fun create(
command: CommandEntity.Create
): Pair<String, PayloadEntity> = remote.create(command)
override suspend fun createDocument(
command: CommandEntity.CreateDocument
@ -66,17 +68,21 @@ class BlockRemoteDataStore(private val remote: BlockRemote) : BlockDataStore {
remote.dnd(command)
}
override suspend fun duplicate(command: CommandEntity.Duplicate) = remote.duplicate(command)
override suspend fun duplicate(
command: CommandEntity.Duplicate
): Pair<String, PayloadEntity> = remote.duplicate(command)
override suspend fun unlink(command: CommandEntity.Unlink) {
remote.unlink(command)
}
override suspend fun unlink(
command: CommandEntity.Unlink
): PayloadEntity = remote.unlink(command)
override suspend fun merge(command: CommandEntity.Merge) {
remote.merge(command)
}
override suspend fun merge(
command: CommandEntity.Merge
): PayloadEntity = remote.merge(command)
override suspend fun split(command: CommandEntity.Split): String = remote.split(command)
override suspend fun split(
command: CommandEntity.Split
): Pair<String, PayloadEntity> = remote.split(command)
override suspend fun setIconName(
command: CommandEntity.SetIconName
@ -94,5 +100,6 @@ class BlockRemoteDataStore(private val remote: BlockRemote) : BlockDataStore {
command: CommandEntity.ArchiveDocument
) = remote.archiveDocument(command)
override suspend fun replace(command: CommandEntity.Replace): Id = remote.replace(command)
override suspend fun replace(command: CommandEntity.Replace): Pair<Id, PayloadEntity> =
remote.replace(command)
}

View file

@ -18,4 +18,12 @@ sealed class Either<out L, out R> {
is Left -> fnL(a)
is Right -> fnR(b)
}
suspend fun proceed(
failure: suspend (L) -> Any,
success: suspend (R) -> Any
): Any = when (this) {
is Left -> failure(a)
is Right -> success(b)
}
}

View file

@ -8,6 +8,7 @@ import com.agileburo.anytype.domain.block.model.Command
import com.agileburo.anytype.domain.block.model.Position
import com.agileburo.anytype.domain.block.repo.BlockRepository
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.domain.event.model.Payload
/**
* Use-case for creating a block.
@ -15,7 +16,7 @@ import com.agileburo.anytype.domain.common.Id
*/
open class CreateBlock(
private val repo: BlockRepository
) : BaseUseCase<Id, Params>() {
) : BaseUseCase<Pair<Id, Payload>, Params>() {
override suspend fun run(params: Params) = try {
repo.create(

View file

@ -5,6 +5,7 @@ import com.agileburo.anytype.domain.base.Either
import com.agileburo.anytype.domain.block.model.Command
import com.agileburo.anytype.domain.block.repo.BlockRepository
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.domain.event.model.Payload
/**
* Use-case for block duplication.
@ -12,7 +13,7 @@ import com.agileburo.anytype.domain.common.Id
*/
open class DuplicateBlock(
private val repo: BlockRepository
) : BaseUseCase<Id, DuplicateBlock.Params>() {
) : BaseUseCase<Pair<Id, Payload>, DuplicateBlock.Params>() {
override suspend fun run(params: Params) = try {
repo.duplicate(

View file

@ -5,12 +5,13 @@ import com.agileburo.anytype.domain.base.Either
import com.agileburo.anytype.domain.block.model.Command
import com.agileburo.anytype.domain.block.repo.BlockRepository
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.domain.event.model.Payload
/**
* Use-case for merging a pair of blocks.
*/
open class MergeBlocks(private val repo: BlockRepository) :
BaseUseCase<Unit, MergeBlocks.Params>() {
BaseUseCase<Payload, MergeBlocks.Params>() {
override suspend fun run(params: Params) = try {
repo.merge(

View file

@ -6,13 +6,14 @@ import com.agileburo.anytype.domain.block.model.Block
import com.agileburo.anytype.domain.block.model.Command
import com.agileburo.anytype.domain.block.repo.BlockRepository
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.domain.event.model.Payload
/**
* Use-case for replacing target block by a new block (created from prototype)
*/
class ReplaceBlock(
private val repo: BlockRepository
) : BaseUseCase<Id, ReplaceBlock.Params>() {
) : BaseUseCase<Pair<Id, Payload>, ReplaceBlock.Params>() {
override suspend fun run(params: Params) = try {
repo.replace(

View file

@ -5,11 +5,14 @@ import com.agileburo.anytype.domain.base.Either
import com.agileburo.anytype.domain.block.model.Command
import com.agileburo.anytype.domain.block.repo.BlockRepository
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.domain.event.model.Payload
/**
* Use-case for splitting the target block into two blocks based on cursor position.
*/
class SplitBlock(private val repo: BlockRepository) : BaseUseCase<Id, SplitBlock.Params>() {
class SplitBlock(
private val repo: BlockRepository
) : BaseUseCase<Pair<Id, Payload>, SplitBlock.Params>() {
override suspend fun run(params: Params) = try {
repo.split(

View file

@ -5,13 +5,14 @@ import com.agileburo.anytype.domain.base.Either
import com.agileburo.anytype.domain.block.model.Command
import com.agileburo.anytype.domain.block.repo.BlockRepository
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.domain.event.model.Payload
/**
* Use-case for unlinking blocks from its context.
* Unlinking is a remplacement for delete operations.
*/
open class UnlinkBlocks(private val repo: BlockRepository) :
BaseUseCase<Unit, UnlinkBlocks.Params>() {
BaseUseCase<Payload, UnlinkBlocks.Params>() {
override suspend fun run(params: Params) = try {
repo.unlink(

View file

@ -6,12 +6,13 @@ import com.agileburo.anytype.domain.block.model.Block
import com.agileburo.anytype.domain.block.model.Command
import com.agileburo.anytype.domain.block.repo.BlockRepository
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.domain.event.model.Payload
open class UpdateBlockAlignment(
open class UpdateAlignment(
private val repo: BlockRepository
) : BaseUseCase<Unit, UpdateBlockAlignment.Params>() {
) : BaseUseCase<Payload, UpdateAlignment.Params>() {
override suspend fun run(params: Params): Either<Throwable, Unit> = try {
override suspend fun run(params: Params) = try {
repo.updateAlignment(
command = Command.UpdateAlignment(
context = params.context,

View file

@ -5,13 +5,14 @@ import com.agileburo.anytype.domain.base.Either
import com.agileburo.anytype.domain.block.model.Command
import com.agileburo.anytype.domain.block.repo.BlockRepository
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.domain.event.model.Payload
/**
* Use-case for updating the whole block's text color.
*/
open class UpdateBackgroundColor(
private val repo: BlockRepository
) : BaseUseCase<Unit, UpdateBackgroundColor.Params>() {
) : BaseUseCase<Payload, UpdateBackgroundColor.Params>() {
override suspend fun run(params: Params) = try {
repo.updateBackgroundColor(

View file

@ -5,6 +5,7 @@ import com.agileburo.anytype.domain.base.Either
import com.agileburo.anytype.domain.block.model.Block
import com.agileburo.anytype.domain.block.model.Command
import com.agileburo.anytype.domain.block.repo.BlockRepository
import com.agileburo.anytype.domain.common.Id
open class UpdateText(
private val repo: BlockRepository
@ -13,8 +14,8 @@ open class UpdateText(
override suspend fun run(params: Params) = try {
repo.updateText(
command = Command.UpdateText(
contextId = params.contextId,
blockId = params.blockId,
contextId = params.context,
blockId = params.target,
text = params.text,
marks = params.marks
)
@ -26,10 +27,9 @@ open class UpdateText(
}
data class Params(
val contextId: String,
val blockId: String,
val context: Id,
val target: Id,
val text: String,
val marks: List<Block.Content.Text.Mark>
)
}

View file

@ -5,13 +5,14 @@ import com.agileburo.anytype.domain.base.Either
import com.agileburo.anytype.domain.block.model.Command
import com.agileburo.anytype.domain.block.repo.BlockRepository
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.domain.event.model.Payload
/**
* Use-case for updating the whole block's text color.
*/
open class UpdateTextColor(
private val repo: BlockRepository
) : BaseUseCase<Unit, UpdateTextColor.Params>() {
) : BaseUseCase<Payload, UpdateTextColor.Params>() {
override suspend fun run(params: Params) = try {
repo.updateTextColor(

View file

@ -32,8 +32,6 @@ data class Block(
val icon: String? by default
val isArchived: Boolean? by default
fun hasName() = map.containsKey(NAME_KEY)
companion object {
fun empty(): Fields = Fields(emptyMap())
const val NAME_KEY = "name"
@ -45,7 +43,7 @@ data class Block(
* Document metadata
* @property details maps id of the block to its details (contained as fields)
*/
data class Details(val details: Map<Id, Fields>)
data class Details(val details: Map<Id, Fields> = emptyMap())
/**
* Block's content.
@ -58,6 +56,15 @@ data class Block(
fun asDivider() = this as Divider
fun asFile() = this as File
/**
* Smart block.
*/
data class Smart(
val type: Type
) : Content() {
enum class Type { HOME, PAGE, ARCHIVE, BREADCRUMBS, PROFILE }
}
/**
* Textual block.
* @property text content text
@ -124,7 +131,7 @@ data class Block(
}
data class Layout(val type: Type) : Content() {
enum class Type { ROW, COLUMN }
enum class Type { ROW, COLUMN, DIV }
}
data class Image(

View file

@ -3,19 +3,25 @@ package com.agileburo.anytype.domain.block.repo
import com.agileburo.anytype.domain.block.model.Command
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.domain.config.Config
import com.agileburo.anytype.domain.event.model.Payload
interface BlockRepository {
suspend fun dnd(command: Command.Dnd)
suspend fun duplicate(command: Command.Duplicate): Id
suspend fun unlink(command: Command.Unlink)
suspend fun unlink(command: Command.Unlink): Payload
suspend fun archiveDocument(command: Command.ArchiveDocument)
/**
* Creates a new block.
* @return id of the created block.
* Duplicates target block
* @return id of the new block and payload events.
*/
suspend fun create(command: Command.Create): Id
suspend fun duplicate(command: Command.Duplicate): Pair<Id, Payload>
/**
* Creates a new block.
* @return id of the created block with event payload.
*/
suspend fun create(command: Command.Create): Pair<Id, Payload>
/**
* Creates a new document / page.
@ -23,37 +29,39 @@ interface BlockRepository {
*/
suspend fun createDocument(command: Command.CreateDocument): Pair<Id, Id>
suspend fun merge(command: Command.Merge)
suspend fun merge(command: Command.Merge): Payload
/**
* Splits one block into two blocks.
* @return id of the block, created as a result of splitting.
*/
suspend fun split(command: Command.Split): Id
suspend fun split(command: Command.Split): Pair<Id, Payload>
/**
* Replaces target block by a new block (created from prototype).
* @see Command.Replace for details
* @return id of the new block
*/
suspend fun replace(command: Command.Replace): Id
suspend fun replace(command: Command.Replace): Pair<Id, Payload>
suspend fun updateDocumentTitle(command: Command.UpdateTitle)
suspend fun updateText(command: Command.UpdateText)
suspend fun updateTextStyle(command: Command.UpdateStyle)
suspend fun updateTextColor(command: Command.UpdateTextColor)
suspend fun updateBackgroundColor(command: Command.UpdateBackgroundColor)
suspend fun updateTextColor(command: Command.UpdateTextColor): Payload
suspend fun updateBackgroundColor(command: Command.UpdateBackgroundColor): Payload
suspend fun updateCheckbox(command: Command.UpdateCheckbox)
suspend fun updateAlignment(command: Command.UpdateAlignment)
suspend fun updateAlignment(command: Command.UpdateAlignment) : Payload
suspend fun getConfig(): Config
@Deprecated("Should be replaced by createDocument() command")
suspend fun createPage(parentId: String): Id
suspend fun openPage(id: String)
suspend fun openPage(id: String): Payload
suspend fun closePage(id: String)
suspend fun openDashboard(contextId: String, id: String)
suspend fun openDashboard(contextId: String, id: String): Payload
suspend fun closeDashboard(id: String)
/**

View file

@ -2,6 +2,7 @@ package com.agileburo.anytype.domain.dashboard.interactor
import com.agileburo.anytype.domain.block.model.Block
import com.agileburo.anytype.domain.dashboard.model.HomeDashboard
import com.agileburo.anytype.domain.ext.content
fun List<Block>.toHomeDashboard(
id: String,
@ -15,7 +16,7 @@ fun List<Block>.toHomeDashboard(
},
children = root.children,
fields = root.fields,
type = root.content.asDashboard().type,
type = root.content<Block.Content.Smart>().type,
details = details
)
}

View file

@ -3,6 +3,7 @@ package com.agileburo.anytype.domain.dashboard.interactor
import com.agileburo.anytype.domain.base.BaseUseCase
import com.agileburo.anytype.domain.base.Either
import com.agileburo.anytype.domain.block.repo.BlockRepository
import com.agileburo.anytype.domain.event.model.Payload
/**
* Use-case for opening a dashboard by sending a special request.
@ -11,7 +12,7 @@ import com.agileburo.anytype.domain.block.repo.BlockRepository
*/
class OpenDashboard(
private val repo: BlockRepository
) : BaseUseCase<Unit, OpenDashboard.Param?>() {
) : BaseUseCase<Payload, OpenDashboard.Param?>() {
override suspend fun run(params: Param?) = try {
if (params != null)

View file

@ -7,6 +7,6 @@ data class HomeDashboard(
val blocks: List<Block>,
val children: List<String>,
val fields: Block.Fields,
val type: Block.Content.Dashboard.Type,
val type: Block.Content.Smart.Type,
val details: Block.Details = Block.Details(emptyMap())
)

View file

@ -0,0 +1,13 @@
package com.agileburo.anytype.domain.event.model
import com.agileburo.anytype.domain.common.Id
/**
* Represent events, as response to some command.
* @param context id of the context
* @param events new events, contained in response.
*/
data class Payload(
val context: Id,
val events: List<Event>
)

View file

@ -4,10 +4,11 @@ import com.agileburo.anytype.domain.base.BaseUseCase
import com.agileburo.anytype.domain.base.Either
import com.agileburo.anytype.domain.block.repo.BlockRepository
import com.agileburo.anytype.domain.config.MainConfig
import com.agileburo.anytype.domain.event.model.Payload
open class OpenPage(
private val repo: BlockRepository
) : BaseUseCase<Unit, OpenPage.Params>() {
) : BaseUseCase<Payload, OpenPage.Params>() {
override suspend fun run(params: Params) = try {
repo.openPage(params.id).let {
@ -20,7 +21,7 @@ open class OpenPage(
/**
* @property id page's id
*/
class Params(val id: String) {
data class Params(val id: String) {
companion object {
fun reference() = Params(id = MainConfig.REFERENCE_PAGE_ID)
}

View file

@ -9,6 +9,7 @@ import com.agileburo.anytype.data.auth.model.BlockEntity
import com.agileburo.anytype.data.auth.model.PositionEntity
import com.google.protobuf.Struct
import com.google.protobuf.Value
import timber.log.Timber
fun Events.Event.Account.Show.toAccountEntity(): AccountEntity {
@ -95,6 +96,15 @@ fun Block.fields(): BlockEntity.Fields = BlockEntity.Fields().also { result ->
}
}
fun Events.SmartBlockType.entity(): BlockEntity.Content.Smart.Type = when (this) {
Events.SmartBlockType.Archive -> BlockEntity.Content.Smart.Type.ARCHIVE
Events.SmartBlockType.Page -> BlockEntity.Content.Smart.Type.PAGE
Events.SmartBlockType.Breadcrumbs -> BlockEntity.Content.Smart.Type.BREADCRUMBS
Events.SmartBlockType.Home -> BlockEntity.Content.Smart.Type.HOME
Events.SmartBlockType.ProfilePage -> BlockEntity.Content.Smart.Type.PROFILE
else -> throw IllegalStateException("Unexpected smart block type: $this")
}
fun Struct.fields(): BlockEntity.Fields = BlockEntity.Fields().also { result ->
fieldsMap.forEach { (key, value) ->
result.map[key] = when (val case = value.kindCase) {
@ -106,33 +116,6 @@ fun Struct.fields(): BlockEntity.Fields = BlockEntity.Fields().also { result ->
}
}
fun Block.dashboard(): BlockEntity.Content.Dashboard = BlockEntity.Content.Dashboard(
type = when {
dashboard.style == Block.Content.Dashboard.Style.Archive -> {
BlockEntity.Content.Dashboard.Type.ARCHIVE
}
dashboard.style == Block.Content.Dashboard.Style.MainScreen -> {
BlockEntity.Content.Dashboard.Type.MAIN_SCREEN
}
else -> throw IllegalStateException("Unexpected dashboard style: ${dashboard.style}")
}
)
fun Block.page(): BlockEntity.Content.Page = BlockEntity.Content.Page(
style = when {
page.style == Block.Content.Page.Style.Empty -> {
BlockEntity.Content.Page.Style.EMPTY
}
page.style == Block.Content.Page.Style.Task -> {
BlockEntity.Content.Page.Style.TASK
}
page.style == Block.Content.Page.Style.Set -> {
BlockEntity.Content.Page.Style.SET
}
else -> throw IllegalStateException("Unexpected page style: ${page.style}")
}
)
fun Block.text(): BlockEntity.Content.Text = BlockEntity.Content.Text(
text = text.text,
marks = text.marks.marksList.marks(),
@ -178,31 +161,20 @@ fun List<Block.Content.Text.Mark>.marks(): List<BlockEntity.Content.Text.Mark> =
}
fun Block.layout(): BlockEntity.Content.Layout = BlockEntity.Content.Layout(
type = when {
layout.style == Block.Content.Layout.Style.Column -> {
BlockEntity.Content.Layout.Type.COLUMN
}
layout.style == Block.Content.Layout.Style.Row -> {
BlockEntity.Content.Layout.Type.ROW
}
type = when (layout.style) {
Block.Content.Layout.Style.Column -> BlockEntity.Content.Layout.Type.COLUMN
Block.Content.Layout.Style.Row -> BlockEntity.Content.Layout.Type.ROW
Block.Content.Layout.Style.Div -> BlockEntity.Content.Layout.Type.DIV
else -> throw IllegalStateException("Unexpected layout style: ${layout.style}")
}
)
fun Block.link(): BlockEntity.Content.Link = BlockEntity.Content.Link(
type = when {
link.style == Block.Content.Link.Style.Page -> {
BlockEntity.Content.Link.Type.PAGE
}
link.style == Block.Content.Link.Style.Dataview -> {
BlockEntity.Content.Link.Type.DATA_VIEW
}
link.style == Block.Content.Link.Style.Archive -> {
BlockEntity.Content.Link.Type.ARCHIVE
}
link.style == Block.Content.Link.Style.Dashboard -> {
BlockEntity.Content.Link.Type.DASHBOARD
}
type = when (link.style) {
Block.Content.Link.Style.Page -> BlockEntity.Content.Link.Type.PAGE
Block.Content.Link.Style.Dataview -> BlockEntity.Content.Link.Type.DATA_VIEW
Block.Content.Link.Style.Archive -> BlockEntity.Content.Link.Type.ARCHIVE
Block.Content.Link.Style.Dashboard -> BlockEntity.Content.Link.Type.DASHBOARD
else -> throw IllegalStateException("Unexpected link style: ${link.style}")
},
target = link.targetBlockId,
@ -264,24 +236,10 @@ fun Block.bookmark(): BlockEntity.Content.Bookmark = BlockEntity.Content.Bookmar
favicon = bookmark.faviconHash.ifEmpty { null }
)
fun List<Block>.blocks(): List<BlockEntity> = mapNotNull { block ->
fun List<Block>.blocks(
types: Map<String, Events.SmartBlockType> = emptyMap()
): List<BlockEntity> = mapNotNull { block ->
when (block.contentCase) {
Block.ContentCase.DASHBOARD -> {
BlockEntity(
id = block.id,
children = block.childrenIdsList.toList(),
fields = block.fields(),
content = block.dashboard()
)
}
Block.ContentCase.PAGE -> {
BlockEntity(
id = block.id,
children = block.childrenIdsList.toList(),
fields = block.fields(),
content = block.page()
)
}
Block.ContentCase.TEXT -> {
BlockEntity(
id = block.id,
@ -338,7 +296,18 @@ fun List<Block>.blocks(): List<BlockEntity> = mapNotNull { block ->
content = block.bookmark()
)
}
Block.ContentCase.SMARTBLOCK -> {
BlockEntity(
id = block.id,
children = block.childrenIdsList,
content = BlockEntity.Content.Smart(
type = types[block.id]?.entity() ?: throw IllegalStateException("Type missing")
),
fields = block.fields()
)
}
else -> {
Timber.d("Ignoring content type: ${block.contentCase}")
null
}
}

View file

@ -2,6 +2,7 @@ package com.agileburo.anytype.middleware.block
import com.agileburo.anytype.data.auth.model.CommandEntity
import com.agileburo.anytype.data.auth.model.ConfigEntity
import com.agileburo.anytype.data.auth.model.PayloadEntity
import com.agileburo.anytype.data.auth.repo.block.BlockRemote
import com.agileburo.anytype.middleware.interactor.Middleware
import com.agileburo.anytype.middleware.toMiddleware
@ -12,9 +13,10 @@ class BlockMiddleware(
override suspend fun getConfig(): ConfigEntity = middleware.config
override suspend fun openDashboard(contextId: String, id: String) {
middleware.openDashboard(contextId, id)
}
override suspend fun openDashboard(
contextId: String,
id: String
): PayloadEntity = middleware.openDashboard(contextId, id)
override suspend fun closeDashboard(id: String) {
middleware.closeDashboard(id)
@ -22,9 +24,7 @@ class BlockMiddleware(
override suspend fun createPage(parentId: String): String = middleware.createPage(parentId)
override suspend fun openPage(id: String) {
middleware.openBlock(id)
}
override suspend fun openPage(id: String): PayloadEntity = middleware.openBlock(id)
override suspend fun closePage(id: String) {
middleware.closePage(id)
@ -51,17 +51,17 @@ class BlockMiddleware(
middleware.updateTextStyle(command)
}
override suspend fun updateTextColor(command: CommandEntity.UpdateTextColor) {
middleware.updateTextColor(command)
}
override suspend fun updateTextColor(
command: CommandEntity.UpdateTextColor
): PayloadEntity = middleware.updateTextColor(command)
override suspend fun updateBackgroundColor(command: CommandEntity.UpdateBackgroundColor) {
middleware.updateBackgroundColor(command)
}
override suspend fun updateBackgroundColor(
command: CommandEntity.UpdateBackgroundColor
): PayloadEntity = middleware.updateBackgroundColor(command)
override suspend fun updateAlignment(command: CommandEntity.UpdateAlignment) {
middleware.updateAlignment(command)
}
override suspend fun updateAlignment(
command: CommandEntity.UpdateAlignment
) : PayloadEntity = middleware.updateAlignment(command)
override suspend fun updateCheckbox(command: CommandEntity.UpdateCheckbox) {
middleware.updateCheckbox(
@ -71,7 +71,9 @@ class BlockMiddleware(
)
}
override suspend fun create(command: CommandEntity.Create): String = middleware.createBlock(
override suspend fun create(
command: CommandEntity.Create
): Pair<String, PayloadEntity> = middleware.createBlock(
command.context,
command.target,
command.position,
@ -82,22 +84,25 @@ class BlockMiddleware(
command: CommandEntity.CreateDocument
): Pair<String, String> = middleware.createDocument(command)
override suspend fun duplicate(
command: CommandEntity.Duplicate
): Pair<String, PayloadEntity> = middleware.duplicate(command)
override suspend fun dnd(command: CommandEntity.Dnd) {
middleware.dnd(command)
}
override suspend fun duplicate(command: CommandEntity.Duplicate): String =
middleware.duplicate(command)
override suspend fun unlink(
command: CommandEntity.Unlink
): PayloadEntity = middleware.unlink(command)
override suspend fun unlink(command: CommandEntity.Unlink) {
middleware.unlink(command)
}
override suspend fun merge(
command: CommandEntity.Merge
): PayloadEntity = middleware.merge(command)
override suspend fun merge(command: CommandEntity.Merge) {
middleware.merge(command)
}
override suspend fun split(command: CommandEntity.Split): String = middleware.split(command)
override suspend fun split(
command: CommandEntity.Split
): Pair<String, PayloadEntity> = middleware.split(command)
override suspend fun setIconName(
command: CommandEntity.SetIconName
@ -117,5 +122,5 @@ class BlockMiddleware(
override suspend fun replace(
command: CommandEntity.Replace
): String = middleware.replace(command)
): Pair<String, PayloadEntity> = middleware.replace(command)
}

View file

@ -3,7 +3,9 @@ package com.agileburo.anytype.middleware.interactor;
import com.agileburo.anytype.data.auth.model.BlockEntity;
import com.agileburo.anytype.data.auth.model.CommandEntity;
import com.agileburo.anytype.data.auth.model.ConfigEntity;
import com.agileburo.anytype.data.auth.model.PayloadEntity;
import com.agileburo.anytype.data.auth.model.PositionEntity;
import com.agileburo.anytype.middleware.BuildConfig;
import com.agileburo.anytype.middleware.model.CreateAccountResponse;
import com.agileburo.anytype.middleware.model.CreateWalletResponse;
import com.agileburo.anytype.middleware.model.SelectAccountResponse;
@ -18,6 +20,7 @@ import anytype.Commands.Rpc.BlockList;
import anytype.Commands.Rpc.Config;
import anytype.Commands.Rpc.Wallet;
import anytype.model.Models;
import anytype.model.Models.Range;
import kotlin.Pair;
import timber.log.Timber;
@ -59,18 +62,24 @@ public class Middleware {
}
public CreateAccountResponse createAccount(String name, String path) throws Exception {
Account.Create.Request request;
// TODO remove hard-coded alpha invite code when no longer needed
String code = "elbrus";
if (path != null) {
request = Account.Create.Request
.newBuilder()
.setName(name)
.setAvatarLocalPath(path)
.setAlphaInviteCode(code)
.build();
} else {
request = Account.Create.Request
.newBuilder()
.setName(name)
.setAlphaInviteCode(code)
.build();
}
@ -124,7 +133,7 @@ public class Middleware {
);
}
public void openDashboard(String contextId, String id) throws Exception {
public PayloadEntity openDashboard(String contextId, String id) throws Exception {
Block.Open.Request request = Block.Open.Request
.newBuilder()
.setContextId(contextId)
@ -133,10 +142,12 @@ public class Middleware {
Timber.d("Opening home dashboard with the following request:\n%s", request.toString());
service.blockOpen(request);
Block.Open.Response response = service.blockOpen(request);
return mapper.toPayload(response.getEvent());
}
public void openBlock(String id) throws Exception {
public PayloadEntity openBlock(String id) throws Exception {
Block.Open.Request request = Block.Open.Request
.newBuilder()
.setBlockId(id)
@ -144,7 +155,9 @@ public class Middleware {
Timber.d("Opening page with the following request:\n%s", request.toString());
service.blockOpen(request);
Block.Open.Response response = service.blockOpen(request);
return mapper.toPayload(response.getEvent());
}
public String createPage(String parentId) throws Exception {
@ -258,7 +271,7 @@ public class Middleware {
service.blockSetTextStyle(request);
}
public void updateTextColor(CommandEntity.UpdateTextColor command) throws Exception {
public PayloadEntity updateTextColor(CommandEntity.UpdateTextColor command) throws Exception {
Block.Set.Text.Color.Request request = Block.Set.Text.Color.Request
.newBuilder()
.setContextId(command.getContext())
@ -268,10 +281,12 @@ public class Middleware {
Timber.d("Updating text color with the following request:\n%s", request.toString());
service.blockSetTextColor(request);
Block.Set.Text.Color.Response response = service.blockSetTextColor(request);
return mapper.toPayload(response.getEvent());
}
public void updateBackgroundColor(CommandEntity.UpdateBackgroundColor command) throws Exception {
public PayloadEntity updateBackgroundColor(CommandEntity.UpdateBackgroundColor command) throws Exception {
BlockList.Set.BackgroundColor.Request request = BlockList.Set.BackgroundColor.Request
.newBuilder()
.setContextId(command.getContext())
@ -281,10 +296,12 @@ public class Middleware {
Timber.d("Updating background color with the following request:\n%s", request.toString());
service.blockSetTextBackgroundColor(request);
BlockList.Set.BackgroundColor.Response response = service.blockSetTextBackgroundColor(request);
return mapper.toPayload(response.getEvent());
}
public void updateAlignment(CommandEntity.UpdateAlignment command) throws Exception {
public PayloadEntity updateAlignment(CommandEntity.UpdateAlignment command) throws Exception {
Models.Block.Align align = mapper.toMiddleware(command.getAlignment());
@ -297,7 +314,9 @@ public class Middleware {
Timber.d("Updating alignment with the following request:\n%s", request.toString());
service.blockSetAlignment(request);
BlockList.Set.Align.Response response = service.blockSetAlignment(request);
return mapper.toPayload(response.getEvent());
}
public void uploadMediaBlockContent(CommandEntity.UploadBlock command) throws Exception {
@ -314,7 +333,7 @@ public class Middleware {
service.blockUpload(request);
}
public String createBlock(
public Pair<String, PayloadEntity> createBlock(
String contextId,
String targetId,
PositionEntity position,
@ -333,14 +352,20 @@ public class Middleware {
.setBlock(model)
.build();
Timber.d("Creating block with the following request:\n%s", request.toString());
if (BuildConfig.DEBUG) {
Timber.d(request.getClass().getName() + "\n" + request.toString());
}
Block.Create.Response response = service.blockCreate(request);
return response.getBlockId();
if (BuildConfig.DEBUG) {
Timber.d(response.getClass().getName() + "\n" + response.toString());
}
return new Pair<>(response.getBlockId(), mapper.toPayload(response.getEvent()));
}
public String replace(CommandEntity.Replace command) throws Exception {
public Pair<String, PayloadEntity> replace(CommandEntity.Replace command) throws Exception {
Models.Block model = factory.create(command.getPrototype());
Block.Create.Request request = Block.Create.Request
@ -351,11 +376,11 @@ public class Middleware {
.setBlock(model)
.build();
Timber.d("Replacing block with the following request:\n%s", request.toString());
Timber.i("Replacing block with the following request:\n%s", request.toString());
Block.Create.Response response = service.blockCreate(request);
return response.getBlockId();
return new Pair<>(response.getBlockId(), mapper.toPayload(response.getEvent()));
}
public Pair<String, String> createDocument(CommandEntity.CreateDocument command) throws Exception {
@ -390,7 +415,7 @@ public class Middleware {
service.blockListMove(request);
}
public String duplicate(CommandEntity.Duplicate command) throws Exception {
public Pair<String, PayloadEntity> duplicate(CommandEntity.Duplicate command) throws Exception {
BlockList.Duplicate.Request request = BlockList.Duplicate.Request
.newBuilder()
.setContextId(command.getContext())
@ -403,10 +428,10 @@ public class Middleware {
BlockList.Duplicate.Response response = service.blockListDuplicate(request);
return response.getBlockIds(0);
return new Pair<>(response.getBlockIds(0), mapper.toPayload(response.getEvent()));
}
public void unlink(CommandEntity.Unlink command) throws Exception {
public PayloadEntity unlink(CommandEntity.Unlink command) throws Exception {
Block.Unlink.Request request = Block.Unlink.Request
.newBuilder()
.setContextId(command.getContext())
@ -415,10 +440,12 @@ public class Middleware {
Timber.d("Unlinking blocks with the following request:\n%s", request.toString());
service.blockUnlink(request);
Block.Unlink.Response response = service.blockUnlink(request);
return mapper.toPayload(response.getEvent());
}
public void merge(CommandEntity.Merge command) throws Exception {
public PayloadEntity merge(CommandEntity.Merge command) throws Exception {
Block.Merge.Request request = Block.Merge.Request
.newBuilder()
.setContextId(command.getContext())
@ -428,22 +455,31 @@ public class Middleware {
Timber.d("Merging blocks with the following request:\n%s", request.toString());
service.blockMerge(request);
Block.Merge.Response response = service.blockMerge(request);
return mapper.toPayload(response.getEvent());
}
public String split(CommandEntity.Split command) throws Exception {
public Pair<String, PayloadEntity> split(CommandEntity.Split command) throws Exception {
Range range = Range
.newBuilder()
.setFrom(command.getIndex())
.setTo(command.getIndex())
.build();
Block.Split.Request request = Block.Split.Request
.newBuilder()
.setBlockId(command.getTarget())
.setContextId(command.getContext())
.setCursorPosition(command.getIndex())
.setRange(range)
.build();
Timber.d("Splitting the target block with the following request:\n%s", request.toString());
Block.Split.Response response = service.blockSplit(request);
return response.getBlockId();
return new Pair<>(response.getBlockId(), mapper.toPayload(response.getEvent()));
}
public void setIconName(CommandEntity.SetIconName command) throws Exception {

View file

@ -2,9 +2,8 @@ package com.agileburo.anytype.middleware.interactor
import anytype.Events
import com.agileburo.anytype.data.auth.event.EventRemoteChannel
import com.agileburo.anytype.data.auth.model.BlockEntity
import com.agileburo.anytype.data.auth.model.EventEntity
import com.agileburo.anytype.middleware.*
import com.agileburo.anytype.middleware.EventProxy
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
@ -36,7 +35,7 @@ class MiddlewareEventChannel(
context: String?
): Flow<List<EventEntity>> = events
.flow()
.filter { context == null || it.contextId == context }
.filter { event -> context == null || event.contextId == context }
.map { event ->
event.messagesList.filter { message ->
supportedEvents.contains(message.valueCase)
@ -46,140 +45,6 @@ class MiddlewareEventChannel(
.map { events -> processEvents(events) }
private fun processEvents(events: List<Pair<String, Events.Event.Message>>): List<EventEntity.Command> {
return events.mapNotNull { (context, event) ->
when (event.valueCase) {
Events.Event.Message.ValueCase.BLOCKADD -> {
EventEntity.Command.AddBlock(
context = context,
blocks = event.blockAdd.blocksList.blocks()
)
}
Events.Event.Message.ValueCase.BLOCKSHOW -> {
EventEntity.Command.ShowBlock(
context = context,
root = event.blockShow.rootId,
blocks = event.blockShow.blocksList.blocks(),
details = BlockEntity.Details(
event.blockShow.detailsList.associate { details ->
details.id to details.details.fields()
}
)
)
}
Events.Event.Message.ValueCase.BLOCKSETTEXT -> {
EventEntity.Command.GranularChange(
context = context,
id = event.blockSetText.id,
text = if (event.blockSetText.hasText())
event.blockSetText.text.value
else null,
style = if (event.blockSetText.hasStyle())
event.blockSetText.style.value.entity()
else
null,
color = if (event.blockSetText.hasColor())
event.blockSetText.color.value
else
null,
marks = if (event.blockSetText.hasMarks())
event.blockSetText.marks.value.marksList.marks()
else
null
)
}
Events.Event.Message.ValueCase.BLOCKSETBACKGROUNDCOLOR -> {
EventEntity.Command.GranularChange(
context = context,
id = event.blockSetBackgroundColor.id,
backgroundColor = event.blockSetBackgroundColor.backgroundColor
)
}
Events.Event.Message.ValueCase.BLOCKSETALIGN -> {
EventEntity.Command.GranularChange(
context = context,
id = event.blockSetAlign.id,
alignment = event.blockSetAlign.align.entity()
)
}
Events.Event.Message.ValueCase.BLOCKDELETE -> {
EventEntity.Command.DeleteBlock(
context = context,
targets = event.blockDelete.blockIdsList.toList()
)
}
Events.Event.Message.ValueCase.BLOCKSETCHILDRENIDS -> {
EventEntity.Command.UpdateStructure(
context = context,
id = event.blockSetChildrenIds.id,
children = event.blockSetChildrenIds.childrenIdsList.toList()
)
}
Events.Event.Message.ValueCase.BLOCKSETDETAILS -> {
EventEntity.Command.UpdateDetails(
context = context,
target = event.blockSetDetails.id,
details = event.blockSetDetails.details.fields()
)
}
Events.Event.Message.ValueCase.BLOCKSETLINK -> {
EventEntity.Command.LinkGranularChange(
context = context,
id = event.blockSetLink.id,
target = event.blockSetLink.targetBlockId.value,
fields = if (event.blockSetLink.hasFields())
event.blockSetLink.fields.value.fields()
else
null
)
}
Events.Event.Message.ValueCase.BLOCKSETFIELDS -> {
EventEntity.Command.UpdateFields(
context = context,
target = event.blockSetFields.id,
fields = event.blockSetFields.fields.fields()
)
}
Events.Event.Message.ValueCase.BLOCKSETFILE -> {
with(event.blockSetFile) {
EventEntity.Command.UpdateBlockFile(
context = context,
id = id,
state = if (hasState()) state.value.entity() else null,
type = if (hasType()) type.value.entity() else null,
name = if (hasName()) name.value else null,
hash = if (hasHash()) hash.value else null,
mime = if (hasMime()) mime.value else null,
size = if (hasSize()) size.value else null
)
}
}
Events.Event.Message.ValueCase.BLOCKSETBOOKMARK -> {
EventEntity.Command.BookmarkGranularChange(
context = context,
target = event.blockSetBookmark.id,
url = if (event.blockSetBookmark.hasUrl())
event.blockSetBookmark.url.value
else null,
title = if (event.blockSetBookmark.hasTitle())
event.blockSetBookmark.title.value
else
null,
description = if (event.blockSetBookmark.hasDescription())
event.blockSetBookmark.description.value
else
null,
imageHash = if (event.blockSetBookmark.hasImageHash())
event.blockSetBookmark.imageHash.value
else
null,
faviconHash = if (event.blockSetBookmark.hasFaviconHash())
event.blockSetBookmark.faviconHash.value
else
null
)
}
else -> null
}
}
return events.mapNotNull { (context, event) -> event.toEntity(context) }
}
}

View file

@ -0,0 +1,147 @@
package com.agileburo.anytype.middleware.interactor
import anytype.Events.Event
import com.agileburo.anytype.data.auth.model.BlockEntity
import com.agileburo.anytype.data.auth.model.EventEntity
import com.agileburo.anytype.middleware.blocks
import com.agileburo.anytype.middleware.entity
import com.agileburo.anytype.middleware.fields
import com.agileburo.anytype.middleware.marks
fun Event.Message.toEntity(
context: String
): EventEntity.Command? = when (valueCase) {
Event.Message.ValueCase.BLOCKADD -> {
EventEntity.Command.AddBlock(
context = context,
blocks = blockAdd.blocksList.blocks()
)
}
Event.Message.ValueCase.BLOCKSHOW -> {
EventEntity.Command.ShowBlock(
context = context,
root = blockShow.rootId,
blocks = blockShow.blocksList.blocks(
types = mapOf(blockShow.rootId to blockShow.type)
),
details = BlockEntity.Details(
blockShow.detailsList.associate { details ->
details.id to details.details.fields()
}
)
)
}
Event.Message.ValueCase.BLOCKSETTEXT -> {
EventEntity.Command.GranularChange(
context = context,
id = blockSetText.id,
text = if (blockSetText.hasText())
blockSetText.text.value
else null,
style = if (blockSetText.hasStyle())
blockSetText.style.value.entity()
else
null,
color = if (blockSetText.hasColor())
blockSetText.color.value
else
null,
marks = if (blockSetText.hasMarks())
blockSetText.marks.value.marksList.marks()
else
null
)
}
Event.Message.ValueCase.BLOCKSETBACKGROUNDCOLOR -> {
EventEntity.Command.GranularChange(
context = context,
id = blockSetBackgroundColor.id,
backgroundColor = blockSetBackgroundColor.backgroundColor
)
}
Event.Message.ValueCase.BLOCKDELETE -> {
EventEntity.Command.DeleteBlock(
context = context,
targets = blockDelete.blockIdsList.toList()
)
}
Event.Message.ValueCase.BLOCKSETCHILDRENIDS -> {
EventEntity.Command.UpdateStructure(
context = context,
id = blockSetChildrenIds.id,
children = blockSetChildrenIds.childrenIdsList.toList()
)
}
Event.Message.ValueCase.BLOCKSETDETAILS -> {
EventEntity.Command.UpdateDetails(
context = context,
target = blockSetDetails.id,
details = blockSetDetails.details.fields()
)
}
Event.Message.ValueCase.BLOCKSETLINK -> {
EventEntity.Command.LinkGranularChange(
context = context,
id = blockSetLink.id,
target = blockSetLink.targetBlockId.value,
fields = if (blockSetLink.hasFields())
blockSetLink.fields.value.fields()
else
null
)
}
Event.Message.ValueCase.BLOCKSETALIGN -> {
EventEntity.Command.GranularChange(
context = context,
id = blockSetAlign.id,
alignment = blockSetAlign.align.entity()
)
}
Event.Message.ValueCase.BLOCKSETFIELDS -> {
EventEntity.Command.UpdateFields(
context = context,
target = blockSetFields.id,
fields = blockSetFields.fields.fields()
)
}
Event.Message.ValueCase.BLOCKSETFILE -> {
with(blockSetFile) {
EventEntity.Command.UpdateBlockFile(
context = context,
id = id,
state = if (hasState()) state.value.entity() else null,
type = if (hasType()) type.value.entity() else null,
name = if (hasName()) name.value else null,
hash = if (hasHash()) hash.value else null,
mime = if (hasMime()) mime.value else null,
size = if (hasSize()) size.value else null
)
}
}
Event.Message.ValueCase.BLOCKSETBOOKMARK -> {
EventEntity.Command.BookmarkGranularChange(
context = context,
target = blockSetBookmark.id,
url = if (blockSetBookmark.hasUrl())
blockSetBookmark.url.value
else null,
title = if (blockSetBookmark.hasTitle())
blockSetBookmark.title.value
else
null,
description = if (blockSetBookmark.hasDescription())
blockSetBookmark.description.value
else
null,
imageHash = if (blockSetBookmark.hasImageHash())
blockSetBookmark.imageHash.value
else
null,
faviconHash = if (blockSetBookmark.hasFaviconHash())
blockSetBookmark.faviconHash.value
else
null
)
}
else -> null
}

View file

@ -34,12 +34,7 @@ class MiddlewareFactory {
}
builder.setFile(file).build()
}
is BlockEntity.Prototype.Page -> {
val page = Block.Content.Page.newBuilder().apply {
style = Block.Content.Page.Style.Empty
}
builder.setPage(page).build()
}
else -> throw IllegalStateException("Unexpected prototype: $prototype")
}
}
}

View file

@ -1,7 +1,9 @@
package com.agileburo.anytype.middleware.interactor
import anytype.Events
import anytype.model.Models.Block
import com.agileburo.anytype.data.auth.model.BlockEntity
import com.agileburo.anytype.data.auth.model.PayloadEntity
import com.agileburo.anytype.data.auth.model.PositionEntity
import com.agileburo.anytype.middleware.toMiddleware
@ -15,6 +17,16 @@ class MiddlewareMapper {
return position.toMiddleware()
}
fun toPayload(response: Events.ResponseEvent): PayloadEntity {
val context = response.contextId
return PayloadEntity(
context = context,
events = response.messagesList.mapNotNull { it.toEntity(context) }
)
}
fun toMiddleware(alignment: BlockEntity.Align): Block.Align {
return alignment.toMiddleware()
}

View file

@ -131,7 +131,7 @@ class MiddlewareTest {
assertEquals(
expected = response.blockId,
actual = result
actual = result.first
)
}

1
presentation-editor/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,55 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
def config = rootProject.extensions.getByName("ext")
compileSdkVersion config["compile_sdk"]
defaultConfig {
minSdkVersion config["min_sdk"]
targetSdkVersion config["target_sdk"]
testInstrumentationRunner config["test_runner"]
}
testOptions.unitTests.includeAndroidResources = true
androidExtensions {
experimental = true
}
}
dependencies {
implementation project(':domain')
implementation project(':core-utils')
implementation project(':core-ui')
implementation project(':library-page-icon-picker-widget')
def applicationDependencies = rootProject.ext.mainApplication
def unitTestDependencies = rootProject.ext.unitTesting
def lib = rootProject.ext.libraryPageIconPicker
implementation(lib.emojiJava) {
exclude group: 'org.json', module: 'json'
}
implementation applicationDependencies.kotlin
implementation applicationDependencies.coroutines
implementation applicationDependencies.viewModel
implementation applicationDependencies.viewModelExtensions
implementation applicationDependencies.timber
testImplementation unitTestDependencies.junit
testImplementation unitTestDependencies.kotlinTest
testImplementation unitTestDependencies.mockitoKotlin
testImplementation unitTestDependencies.coroutineTesting
testImplementation unitTestDependencies.liveDataTesting
testImplementation unitTestDependencies.archCoreTesting
androidTestImplementation 'androidx.test:core:1.2.0'
}

View file

21
presentation-editor/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1 @@
<manifest package="com.agileburo.anytype.presentation_editor" />

View file

@ -0,0 +1,36 @@
package com.agileburo.anytype.presentation.desktop
import com.agileburo.anytype.domain.dashboard.interactor.toHomeDashboard
import com.agileburo.anytype.domain.event.model.Event
interface HomeDashboardEventConverter {
fun convert(event: Event): HomeDashboardStateMachine.Event?
class DefaultConverter : HomeDashboardEventConverter {
override fun convert(event: Event) = when (event) {
is Event.Command.UpdateStructure -> HomeDashboardStateMachine.Event.OnStructureUpdated(
event.children
)
is Event.Command.AddBlock -> HomeDashboardStateMachine.Event.OnBlocksAdded(
event.blocks
)
is Event.Command.ShowBlock -> HomeDashboardStateMachine.Event.OnDashboardLoaded(
dashboard = event.blocks.toHomeDashboard(
id = event.context,
details = event.details
)
)
is Event.Command.LinkGranularChange -> {
event.fields?.let { fields ->
HomeDashboardStateMachine.Event.OnLinkFieldsChanged(
id = event.id,
fields = fields
)
}
}
else -> null
}
}
}

View file

@ -14,7 +14,6 @@ import com.agileburo.anytype.domain.block.model.Position
import com.agileburo.anytype.domain.config.GetConfig
import com.agileburo.anytype.domain.dashboard.interactor.CloseDashboard
import com.agileburo.anytype.domain.dashboard.interactor.OpenDashboard
import com.agileburo.anytype.domain.dashboard.interactor.toHomeDashboard
import com.agileburo.anytype.domain.event.interactor.InterceptEvents
import com.agileburo.anytype.domain.event.model.Event
import com.agileburo.anytype.domain.image.LoadImage
@ -38,8 +37,10 @@ class HomeDashboardViewModel(
private val createPage: CreatePage,
private val getConfig: GetConfig,
private val dragAndDrop: DragAndDrop,
private val interceptEvents: InterceptEvents
private val interceptEvents: InterceptEvents,
private val eventConverter: HomeDashboardEventConverter
) : ViewStateViewModel<State>(),
HomeDashboardEventConverter by eventConverter,
SupportNavigation<EventWrapper<AppNavigation.Command>> {
private val machine = Interactor(scope = viewModelScope)
@ -67,55 +68,19 @@ class HomeDashboardViewModel(
}
private fun startInterceptingEvents(context: String) {
// TODO use context when middleware is ready
interceptEvents
.build(InterceptEvents.Params(context = null))
.build(InterceptEvents.Params(context = context))
.onEach { Timber.d("New events: $it") }
.onEach { events ->
events.forEach { event ->
when (event) {
is Event.Command.UpdateStructure -> machine.onEvent(
Machine.Event.OnStructureUpdated(
event.children
)
)
is Event.Command.AddBlock -> machine.onEvent(
Machine.Event.OnBlocksAdded(
event.blocks
)
)
is Event.Command.ShowBlock -> {
if (event.root == context) {
machine.onEvent(
Machine.Event.OnDashboardLoaded(
dashboard = event.blocks.toHomeDashboard(
id = context,
details = event.details
)
)
)
} else {
Timber.e("Receiving event from other context!")
}
}
is Event.Command.LinkGranularChange -> {
event.fields?.let { fields ->
machine.onEvent(
Machine.Event.OnLinkFieldsChanged(
id = event.id,
fields = fields
)
)
}
}
}
}
}
.onEach { events -> processEvents(events) }
.launchIn(viewModelScope)
}
private fun processEvents(events: List<Event>) = events.forEach {
convert(it)?.let { result -> machine.onEvent(result) }
}
private fun proceedWithGettingConfig() {
getConfig.invoke(viewModelScope, Unit) { result ->
getConfig(viewModelScope, Unit) { result ->
result.either(
fnR = { config ->
startInterceptingEvents(context = config.home)
@ -127,7 +92,7 @@ class HomeDashboardViewModel(
}
private fun proceedWithGettingAccount() {
getCurrentAccount.invoke(viewModelScope, BaseUseCase.None) { result ->
getCurrentAccount(viewModelScope, BaseUseCase.None) { result ->
result.either(
fnL = { Timber.e(it, "Error while getting account") },
fnR = { account ->
@ -153,7 +118,7 @@ class HomeDashboardViewModel(
)
}
.collect { param ->
dragAndDrop.invoke(this, param) { result ->
dragAndDrop(viewModelScope, param) { result ->
result.either(
fnL = { Timber.e(it, "Error while DND for: $param") },
fnR = { Timber.d("Successfull DND for: $param") }
@ -164,16 +129,15 @@ class HomeDashboardViewModel(
}
private fun proceedWithOpeningHomeDashboard() {
machine.onEvent(Machine.Event.OnDashboardLoadingStarted)
Timber.d("Opening home dashboard")
// TODO replace params = null by more explicit code
openDashboard.invoke(
scope = viewModelScope,
params = null
) { result ->
result.either(
fnL = { Timber.e(it, "Error while opening home dashboard") },
fnR = { Timber.d("Home dashboard opened") }
viewModelScope.launch {
openDashboard(params = null).either(
fnR = { payload -> processEvents(payload.events) },
fnL = { Timber.e(it, "Error while opening home dashboard") }
)
}
}
@ -241,7 +205,7 @@ class HomeDashboardViewModel(
}
private fun navigateToPage(id: String) {
closeDashboard.invoke(viewModelScope, CloseDashboard.Param.home()) { result ->
closeDashboard(viewModelScope, CloseDashboard.Param.home()) { result ->
result.either(
fnL = { e -> Timber.e(e, "Error while closing a dashobard") },
fnR = { navigation.postValue(EventWrapper(AppNavigation.Command.OpenPage(id))) }

View file

@ -19,7 +19,8 @@ class HomeDashboardViewModelFactory(
private val createPage: CreatePage,
private val getConfig: GetConfig,
private val dnd: DragAndDrop,
private val interceptEvents: InterceptEvents
private val interceptEvents: InterceptEvents,
private val eventConverter: HomeDashboardEventConverter
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
@ -32,7 +33,8 @@ class HomeDashboardViewModelFactory(
createPage = createPage,
getConfig = getConfig,
dragAndDrop = dnd,
interceptEvents = interceptEvents
interceptEvents = interceptEvents,
eventConverter = eventConverter
) as T
}
}

View file

@ -5,7 +5,6 @@ import com.agileburo.anytype.core_ui.features.page.BlockView
import com.agileburo.anytype.core_ui.model.UiBlock
import com.agileburo.anytype.domain.block.model.Block
import com.agileburo.anytype.domain.dashboard.model.HomeDashboard
import com.agileburo.anytype.domain.emoji.Emojifier
import com.agileburo.anytype.domain.misc.UrlBuilder
import com.agileburo.anytype.presentation.desktop.DashboardView
@ -133,9 +132,7 @@ fun Block.Content.Text.marks(): List<Markup.Mark> = marks.mapNotNull { mark ->
}
}
suspend fun HomeDashboard.toView(
emojifier: Emojifier
): List<DashboardView.Document> = children.mapNotNull { id ->
fun HomeDashboard.toView(): List<DashboardView.Document> = children.mapNotNull { id ->
blocks.find { block -> block.id == id }?.let { model ->
when (val content = model.content) {
is Block.Content.Link -> {
@ -146,7 +143,7 @@ suspend fun HomeDashboard.toView(
title = details.details[content.target]?.name,
emoji = details.details[content.target]?.icon?.let { name ->
if (name.isNotEmpty())
emojifier.fromShortName(name).unicode
name
else
null
}

View file

@ -0,0 +1,24 @@
package com.agileburo.anytype.presentation.page
import com.agileburo.anytype.presentation.page.editor.Proxy
import com.agileburo.anytype.presentation.page.editor.Store
import com.agileburo.anytype.presentation.page.selection.SelectionStateHolder
interface Editor {
class Storage {
val focus: Store<String> = Store.Focus()
val details: Store.Details = Store.Details()
}
class Proxer(
val intents: Proxy.Intents = Proxy.Intents(),
val changes: Proxy.Text.Changes = Proxy.Text.Changes(),
val saves: Proxy.Text.Saves = Proxy.Text.Saves(),
val payloads: Proxy.Payloads = Proxy.Payloads()
)
class Memory(
val selections: SelectionStateHolder
)
}

View file

@ -2,19 +2,17 @@ package com.agileburo.anytype.presentation.page
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.agileburo.anytype.core_ui.features.page.pattern.Matcher
import com.agileburo.anytype.core_ui.features.page.pattern.Pattern
import com.agileburo.anytype.core_utils.tools.Counter
import com.agileburo.anytype.domain.block.interactor.*
import com.agileburo.anytype.domain.block.interactor.RemoveLinkMark
import com.agileburo.anytype.domain.block.interactor.UpdateLinkMarks
import com.agileburo.anytype.domain.block.interactor.UploadUrl
import com.agileburo.anytype.domain.block.model.Block
import com.agileburo.anytype.domain.download.DownloadFile
import com.agileburo.anytype.domain.event.interactor.InterceptEvents
import com.agileburo.anytype.domain.event.model.Event
import com.agileburo.anytype.domain.misc.UrlBuilder
import com.agileburo.anytype.domain.page.*
import com.agileburo.anytype.presentation.common.StateReducer
import com.agileburo.anytype.presentation.page.editor.Orchestrator
import com.agileburo.anytype.presentation.page.render.DefaultBlockViewRenderer
import com.agileburo.anytype.presentation.page.selection.SelectionStateHolder
open class PageViewModelFactory(
private val openPage: OpenPage,
@ -22,32 +20,14 @@ open class PageViewModelFactory(
private val createPage: CreatePage,
private val createDocument: CreateDocument,
private val archiveDocument: ArchiveDocument,
private val redo: Redo,
private val undo: Undo,
private val updateText: UpdateText,
private val createBlock: CreateBlock,
private val replaceBlock: ReplaceBlock,
private val interceptEvents: InterceptEvents,
private val updateCheckbox: UpdateCheckbox,
private val unlinkBlocks: UnlinkBlocks,
private val duplicateBlock: DuplicateBlock,
private val updateTextStyle: UpdateTextStyle,
private val updateTextColor: UpdateTextColor,
private val updateBackgroundColor: UpdateBackgroundColor,
private val updateLinkMarks: UpdateLinkMarks,
private val removeLinkMark: RemoveLinkMark,
private val mergeBlocks: MergeBlocks,
private val uploadUrl: UploadUrl,
private val splitBlock: SplitBlock,
private val documentEventReducer: StateReducer<List<Block>, Event>,
private val urlBuilder: UrlBuilder,
private val downloadFile: DownloadFile,
private val renderer: DefaultBlockViewRenderer,
private val counter: Counter,
private val patternMatcher: Matcher<Pattern>,
private val updateTitle: UpdateTitle,
private val selectionStateHolder: SelectionStateHolder,
private val updateAlignment: UpdateBlockAlignment
private val interactor: Orchestrator
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
@ -55,35 +35,17 @@ open class PageViewModelFactory(
return PageViewModel(
openPage = openPage,
closePage = closePage,
undo = undo,
redo = redo,
updateText = updateText,
createBlock = createBlock,
archiveDocument = archiveDocument,
interceptEvents = interceptEvents,
updateCheckbox = updateCheckbox,
duplicateBlock = duplicateBlock,
unlinkBlocks = unlinkBlocks,
updateTextStyle = updateTextStyle,
updateTextColor = updateTextColor,
updateBackgroundColor = updateBackgroundColor,
updateLinkMarks = updateLinkMarks,
removeLinkMark = removeLinkMark,
mergeBlocks = mergeBlocks,
splitBlock = splitBlock,
uploadUrl = uploadUrl,
createPage = createPage,
documentExternalEventReducer = documentEventReducer,
reducer = documentEventReducer,
urlBuilder = urlBuilder,
downloadFile = downloadFile,
renderer = renderer,
counter = counter,
createDocument = createDocument,
replaceBlock = replaceBlock,
patternMatcher = patternMatcher,
updateTitle = updateTitle,
selectionStateHolder = selectionStateHolder,
updateAlignment = updateAlignment
orchestrator = interactor
) as T
}
}

View file

@ -0,0 +1,44 @@
package com.agileburo.anytype.presentation.page.editor
import com.agileburo.anytype.core_ui.features.page.BlockView
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.domain.common.Url
sealed class Command {
data class OpenPagePicker(
val target: String
) : Command()
data class OpenGallery(
val mediaType: String
) : Command()
data class OpenBookmarkSetter(
val target: String,
val context: String
) : Command()
object OpenAddBlockPanel : Command()
data class OpenTurnIntoPanel(
val target: Id
) : Command()
object OpenMultiSelectTurnIntoPanel : Command()
data class RequestDownloadPermission(
val id: String
) : Command()
object PopBackStack : Command()
object CloseKeyboard : Command()
data class OpenActionBar(
val block: BlockView
) : Command()
data class Browse(
val url: Url
) : Command()
}

View file

@ -0,0 +1,5 @@
package com.agileburo.anytype.presentation.page.editor
interface Converter<in INPUT, out OUTPUT> {
fun convert(input: INPUT): OUTPUT
}

View file

@ -0,0 +1,114 @@
package com.agileburo.anytype.presentation.page.editor
import com.agileburo.anytype.domain.block.model.Block
import com.agileburo.anytype.domain.block.model.Position
import com.agileburo.anytype.domain.common.Id
sealed class Intent {
sealed class Document : Intent() {
class Undo(
val context: Id
) : Document()
class Redo(
val context: Id
) : Document()
class UpdateTitle(
val context: Id,
val title: String
) : Document()
}
sealed class CRUD : Intent() {
class Replace(
val context: Id,
val target: Id,
val prototype: Block.Prototype
) : CRUD()
class Create(
val context: Id,
val target: Id,
val position: Position,
val prototype: Block.Prototype
) : CRUD()
class Duplicate(
val context: Id,
val target: Id
) : CRUD()
class Unlink(
val context: Id,
val targets: List<Id>,
val previous: Id?,
val next: Id?,
val effects: List<SideEffect> = emptyList()
) : CRUD()
}
sealed class Text : Intent() {
class UpdateColor(
val context: Id,
val target: Id,
val color: String
) : Text()
class UpdateBackgroundColor(
val context: Id,
val targets: List<Id>,
val color: String
) : Text()
class Split(
val context: Id,
val target: Id,
val index: Int
) : Text()
class Merge(
val context: Id,
val previous: Id,
val pair: Pair<Id, Id>
) : Text()
class UpdateStyle(
val context: Id,
val targets: List<Id>,
val style: Block.Content.Text.Style
) : Text()
class UpdateCheckbox(
val context: Id,
val target: Id,
val isChecked: Boolean
) : Text()
class UpdateText(
val context: Id,
val target: Id,
val text: String,
val marks: List<Block.Content.Text.Mark>
) : Text()
class Align(
val context: Id,
val target: Id,
val alignment: Block.Align
) : Text()
}
sealed class Media : Intent() {
class DownloadFile(
val url: String,
val name: String
) : Media()
}
}

View file

@ -0,0 +1,105 @@
package com.agileburo.anytype.presentation.page.editor
import com.agileburo.anytype.core_ui.features.page.pattern.Matcher
import com.agileburo.anytype.core_ui.features.page.pattern.Pattern
import com.agileburo.anytype.domain.block.model.Block
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.presentation.page.Editor
import com.agileburo.anytype.presentation.page.model.TextUpdate
interface Interactor {
class TextInteractor(
private val proxies: Editor.Proxer,
private val stores: Editor.Storage,
private val matcher: Matcher<Pattern>
) {
suspend fun consume(update: TextUpdate, context: Id) {
if (update is TextUpdate.Default)
proxies.saves.send(update)
else if (update is TextUpdate.Pattern) {
val patterns = matcher.match(update.text)
when {
patterns.isEmpty() -> proxies.saves.send(update)
patterns.contains(Pattern.NUMBERED) -> replaceBy(
context = context,
target = update.target,
prototype = Block.Prototype.Text(
style = Block.Content.Text.Style.NUMBERED
)
)
patterns.contains(Pattern.CHECKBOX) -> replaceBy(
context = context,
target = update.target,
prototype = Block.Prototype.Text(
style = Block.Content.Text.Style.CHECKBOX
)
)
patterns.contains(Pattern.BULLET) -> replaceBy(
context = context,
target = update.target,
prototype = Block.Prototype.Text(
style = Block.Content.Text.Style.BULLET
)
)
patterns.contains(Pattern.H1) -> replaceBy(
context = context,
target = update.target,
prototype = Block.Prototype.Text(
style = Block.Content.Text.Style.H1
)
)
patterns.contains(Pattern.H2) -> replaceBy(
context = context,
target = update.target,
prototype = Block.Prototype.Text(
style = Block.Content.Text.Style.H2
)
)
patterns.contains(Pattern.H3) -> replaceBy(
context = context,
target = update.target,
prototype = Block.Prototype.Text(
style = Block.Content.Text.Style.H3
)
)
patterns.contains(Pattern.QUOTE) -> replaceBy(
context = context,
target = update.target,
prototype = Block.Prototype.Text(
style = Block.Content.Text.Style.QUOTE
)
)
patterns.contains(Pattern.TOGGLE) -> replaceBy(
context = context,
target = update.target,
prototype = Block.Prototype.Text(
style = Block.Content.Text.Style.TOGGLE
)
)
patterns.contains(Pattern.DIVIDER) -> replaceBy(
context = context,
target = update.target,
prototype = Block.Prototype.Divider
)
else -> proxies.saves.send(update)
}
}
}
private suspend fun replaceBy(
context: Id,
target: Id,
prototype: Block.Prototype
) {
proxies.intents.send(
Intent.CRUD.Replace(
context = context,
target = target,
prototype = prototype
)
)
}
}
}

View file

@ -0,0 +1,255 @@
package com.agileburo.anytype.presentation.page.editor
import com.agileburo.anytype.domain.block.interactor.*
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.domain.download.DownloadFile
import com.agileburo.anytype.domain.event.model.Payload
import com.agileburo.anytype.domain.page.Redo
import com.agileburo.anytype.domain.page.Undo
import com.agileburo.anytype.domain.page.UpdateTitle
import com.agileburo.anytype.presentation.page.Editor
import kotlinx.coroutines.flow.collect
import timber.log.Timber
class Orchestrator(
private val createBlock: CreateBlock,
private val replaceBlock: ReplaceBlock,
private val updateTextColor: UpdateTextColor,
private val updateBackgroundColor: UpdateBackgroundColor,
private val duplicateBlock: DuplicateBlock,
private val splitBlock: SplitBlock,
private val mergeBlocks: MergeBlocks,
private val unlinkBlocks: UnlinkBlocks,
private val updateTextStyle: UpdateTextStyle,
private val updateCheckbox: UpdateCheckbox,
private val updateTitle: UpdateTitle,
private val downloadFile: DownloadFile,
private val updateText: UpdateText,
private val updateAlignment: UpdateAlignment,
private val undo: Undo,
private val redo: Redo,
val memory: Editor.Memory,
val stores: Editor.Storage,
val proxies: Editor.Proxer,
val textInteractor: Interactor.TextInteractor
) {
private val defaultOnSuccess: suspend (Pair<Id, Payload>) -> Unit = { (id, payload) ->
stores.focus.update(id)
proxies.payloads.send(payload)
}
private val defaultOnError: suspend (Throwable) -> Unit = { Timber.e(it) }
suspend fun start() {
proxies.intents.stream().collect { intent ->
when (intent) {
is Intent.CRUD.Create -> {
createBlock(
params = CreateBlock.Params(
context = intent.context,
target = intent.target,
prototype = intent.prototype,
position = intent.position
)
).proceed(
failure = defaultOnError,
success = defaultOnSuccess
)
}
is Intent.CRUD.Replace -> {
replaceBlock(
params = ReplaceBlock.Params(
context = intent.context,
target = intent.target,
prototype = intent.prototype
)
).proceed(
failure = defaultOnError,
success = defaultOnSuccess
)
}
is Intent.CRUD.Duplicate -> {
duplicateBlock(
params = DuplicateBlock.Params(
context = intent.context,
original = intent.target
)
).proceed(
failure = defaultOnError,
success = defaultOnSuccess
)
}
is Intent.CRUD.Unlink -> {
unlinkBlocks(
params = UnlinkBlocks.Params(
context = intent.context,
targets = intent.targets
)
).proceed(
failure = defaultOnError,
success = { payload ->
val focus = intent.previous ?: intent.next
if (focus != null) stores.focus.update(focus)
processSideEffects(intent.effects)
proxies.payloads.send(payload)
}
)
}
is Intent.Text.Split -> {
splitBlock(
params = SplitBlock.Params(
context = intent.context,
target = intent.target,
index = intent.index
)
).proceed(
failure = defaultOnError,
success = { (id, payload) ->
stores.focus.update(intent.target)
proxies.payloads.send(payload)
}
)
}
is Intent.Text.Merge -> {
mergeBlocks(
params = MergeBlocks.Params(
context = intent.context,
pair = intent.pair
)
).proceed(
failure = defaultOnError,
success = { payload ->
stores.focus.update(intent.previous)
proxies.payloads.send(payload)
}
)
}
is Intent.Text.UpdateColor -> {
updateTextColor(
params = UpdateTextColor.Params(
context = intent.context,
target = intent.target,
color = intent.color
)
).proceed(
failure = defaultOnError,
success = { payload -> proxies.payloads.send(payload) }
)
}
is Intent.Text.UpdateBackgroundColor -> {
updateBackgroundColor(
params = UpdateBackgroundColor.Params(
context = intent.context,
targets = intent.targets,
color = intent.color
)
).proceed(
failure = defaultOnError,
success = { payload -> proxies.payloads.send(payload) }
)
}
is Intent.Text.UpdateStyle -> {
updateTextStyle(
params = UpdateTextStyle.Params(
context = intent.context,
targets = intent.targets,
style = intent.style
)
).proceed(
failure = defaultOnError,
success = {}
)
}
is Intent.Text.UpdateCheckbox -> {
updateCheckbox(
params = UpdateCheckbox.Params(
context = intent.context,
target = intent.target,
isChecked = intent.isChecked
)
).proceed(
failure = defaultOnError,
success = {}
)
}
is Intent.Text.UpdateText -> {
updateText(
params = UpdateText.Params(
context = intent.context,
target = intent.target,
text = intent.text,
marks = intent.marks
)
).proceed(
failure = defaultOnError,
success = {}
)
}
is Intent.Text.Align -> {
updateAlignment(
params = UpdateAlignment.Params(
context = intent.context,
targets = listOf(intent.target),
alignment = intent.alignment
)
).proceed(
failure = defaultOnError,
success = { payload ->
proxies.payloads.send(payload)
}
)
}
is Intent.Document.Redo -> {
redo(
params = Redo.Params(
context = intent.context
)
).proceed(
failure = defaultOnError,
success = { }
)
}
is Intent.Document.Undo -> {
undo(
params = Undo.Params(
context = intent.context
)
).proceed(
failure = defaultOnError,
success = {}
)
}
is Intent.Document.UpdateTitle -> {
updateTitle(
params = UpdateTitle.Params(
context = intent.context,
title = intent.title
)
).proceed(
failure = defaultOnError,
success = {}
)
}
is Intent.Media.DownloadFile -> {
downloadFile(
params = DownloadFile.Params(
url = intent.url,
name = intent.name
)
).proceed(
failure = defaultOnError,
success = {}
)
}
}
}
}
private fun processSideEffects(effects: List<SideEffect>) {
effects.forEach { effect ->
if (effect is SideEffect.ClearMultiSelectSelection)
memory.selections.clearSelections()
}
}
}

View file

@ -0,0 +1,41 @@
package com.agileburo.anytype.presentation.page.editor
import com.agileburo.anytype.domain.event.model.Payload
import com.agileburo.anytype.presentation.page.model.TextUpdate
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.consumeAsFlow
interface Proxy<T> {
/**
* @return streams of values
*/
fun stream(): Flow<T>
/**
* Updates current values
*/
suspend fun send(t: T)
fun cancel()
open class Subject<T> : Proxy<T> {
private val channel = Channel<T>()
private val stream = channel.consumeAsFlow()
override fun stream(): Flow<T> = stream
override suspend fun send(t: T) = channel.send(t)
override fun cancel() = channel.cancel()
}
sealed class Text : Subject<TextUpdate>() {
class Changes : Text()
class Saves : Text()
}
class Intents : Subject<Intent>()
class Payloads : Subject<Payload>()
}

View file

@ -0,0 +1,5 @@
package com.agileburo.anytype.presentation.page.editor
sealed class SideEffect {
object ClearMultiSelectSelection : SideEffect()
}

View file

@ -0,0 +1,52 @@
package com.agileburo.anytype.presentation.page.editor
import com.agileburo.anytype.domain.block.model.Block
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.presentation.page.PageViewModel
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
/**
* Reactive store
* @param T stored type
*/
interface Store<T> {
/**
* @return streams of values
*/
fun stream(): Flow<T>
/**
* @return current/last value
*/
fun current(): T
/**
* Updates current values
*/
suspend fun update(t: T)
fun cancel()
open class Conflated<T>(default: T) : Store<T> {
private val channel = ConflatedBroadcastChannel(default)
private val stream = channel.asFlow()
override fun stream(): Flow<T> = stream
override fun current(): T = channel.value
override suspend fun update(t: T) = channel.send(t)
override fun cancel() = channel.cancel()
}
class Focus : Conflated<String>(PageViewModel.EMPTY_FOCUS_ID)
class Context : Conflated<String>("")
class Details : Conflated<Block.Details>(Block.Details()) {
suspend fun add(target: Id, fields: Block.Fields) {
update(current().copy(details = current().details + mapOf(target to fields)))
}
}
}

View file

@ -0,0 +1,114 @@
package com.agileburo.anytype.presentation.page.editor
import com.agileburo.anytype.core_ui.common.Markup
import com.agileburo.anytype.domain.block.model.Block
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.domain.ext.addMark
import com.agileburo.anytype.domain.ext.content
import com.agileburo.anytype.domain.misc.Reducer
import com.agileburo.anytype.presentation.page.model.TextUpdate
sealed class Transformation {
data class ApplyMarkup(
val target: Id,
val type: Markup.Type,
val range: IntRange,
val param: String?
) : Reducer<List<Block>, ApplyMarkup> {
override fun reduce(state: List<Block>, event: ApplyMarkup): List<Block> {
val target = state.first { it.id == target }
val content = target.content<Block.Content.Text>()
val mark = Block.Content.Text.Mark(
range = range,
type = when (type) {
Markup.Type.BOLD -> Block.Content.Text.Mark.Type.BOLD
Markup.Type.ITALIC -> Block.Content.Text.Mark.Type.ITALIC
Markup.Type.STRIKETHROUGH -> Block.Content.Text.Mark.Type.STRIKETHROUGH
Markup.Type.TEXT_COLOR -> Block.Content.Text.Mark.Type.TEXT_COLOR
Markup.Type.LINK -> Block.Content.Text.Mark.Type.LINK
Markup.Type.BACKGROUND_COLOR -> Block.Content.Text.Mark.Type.BACKGROUND_COLOR
Markup.Type.KEYBOARD -> Block.Content.Text.Mark.Type.KEYBOARD
},
param = param
)
val marks = content.marks.addMark(mark)
val new = target.copy(
content = content.copy(marks = marks)
)
return state.map { block ->
if (block.id != event.target)
block
else
new
}
}
}
data class UpdateText(
val update: TextUpdate
) : Reducer<List<Block>, UpdateText> {
override fun reduce(
state: List<Block>,
event: UpdateText
): List<Block> = state.map { block ->
if (block.id == update.target) {
block.copy(
content = block.content<Block.Content.Text>().copy(
text = update.text,
marks = update.markup.filter {
it.range.first != it.range.last
}
)
)
} else
block
}
}
}
fun Block.updateText(update: TextUpdate): Block {
return copy(
content = content<Block.Content.Text>().copy(
text = update.text,
marks = update.markup.filter {
it.range.first != it.range.last
}
)
)
}
fun Block.markup(
type: Markup.Type,
range: IntRange,
param: String?
): Block {
val content = content<Block.Content.Text>()
val mark = Block.Content.Text.Mark(
range = range,
type = when (type) {
Markup.Type.BOLD -> Block.Content.Text.Mark.Type.BOLD
Markup.Type.ITALIC -> Block.Content.Text.Mark.Type.ITALIC
Markup.Type.STRIKETHROUGH -> Block.Content.Text.Mark.Type.STRIKETHROUGH
Markup.Type.TEXT_COLOR -> Block.Content.Text.Mark.Type.TEXT_COLOR
Markup.Type.LINK -> Block.Content.Text.Mark.Type.LINK
Markup.Type.BACKGROUND_COLOR -> Block.Content.Text.Mark.Type.BACKGROUND_COLOR
Markup.Type.KEYBOARD -> Block.Content.Text.Mark.Type.KEYBOARD
},
param = param
)
val marks = content.marks.addMark(mark)
return copy(content = content.copy(marks = marks))
}

View file

@ -0,0 +1,15 @@
package com.agileburo.anytype.presentation.page.editor
import com.agileburo.anytype.core_ui.features.page.BlockView
import com.agileburo.anytype.domain.block.model.Block
sealed class ViewState {
object Loading : ViewState()
data class Success(val blocks: List<BlockView>) : ViewState()
data class Error(val message: String) : ViewState()
data class OpenLinkScreen(
val pageId: String,
val block: Block,
val range: IntRange
) : ViewState()
}

View file

@ -1,6 +1,5 @@
package com.agileburo.anytype.presentation.page.model
import com.agileburo.anytype.core_ui.features.page.pattern.Pattern
import com.agileburo.anytype.domain.block.model.Block
import com.agileburo.anytype.domain.common.Id
@ -9,11 +8,34 @@ import com.agileburo.anytype.domain.common.Id
* @property target id of the text block, in which this change occurs
* @property text new text for this [target]
* @property markup markup, associated with this [text]
* @property patterns editor patterns found in this [text]
*/
class TextUpdate(
val target: Id,
val text: String,
val markup: List<Block.Content.Text.Mark>,
val patterns: List<Pattern>
)
sealed class TextUpdate {
abstract val target: Id
abstract val text: String
abstract val markup: List<Block.Content.Text.Mark>
/**
* Default text update.
* @property target id of the text block, in which this change occurs
* @property text new text for this [target]
* @property markup markup, associated with this [text]
*/
data class Default(
override val target: Id,
override val text: String,
override val markup: List<Block.Content.Text.Mark>
) : TextUpdate()
/**
* Text update qui may contain patterns we need to detect.
* @property target id of the text block, in which this change occurs
* @property text new text for this [target]
* @property markup markup, associated with this [text]
*/
data class Pattern(
override val target: Id,
override val text: String,
override val markup: List<Block.Content.Text.Mark>
) : TextUpdate()
}

View file

@ -111,7 +111,7 @@ class PageIconPickerViewModel(
setIconName.run(
params = SetIconName.Params(
target = action.target,
name = ":${action.alias}:",
name = action.unicode,
context = action.context
)
)

View file

@ -1,7 +1,6 @@
package com.agileburo.anytype.presentation.page.render
import com.agileburo.anytype.core_ui.features.page.BlockView
import com.agileburo.anytype.core_utils.tools.Counter
import com.agileburo.anytype.domain.block.model.Block
import com.agileburo.anytype.domain.common.Id
import com.agileburo.anytype.domain.page.EditorMode
@ -24,7 +23,6 @@ interface BlockViewRenderer {
focus: Id,
anchor: Id,
indent: Int,
counter: Counter,
details: Block.Details = Block.Details(emptyMap())
): List<BlockView>
}

View file

@ -12,8 +12,8 @@ import com.agileburo.anytype.presentation.mapper.*
import com.agileburo.anytype.presentation.page.toggle.ToggleStateHolder
class DefaultBlockViewRenderer(
private val counter: Counter,
private val urlBuilder: UrlBuilder,
private val emojifier: Emojifier,
private val toggleStateHolder: ToggleStateHolder
) : BlockViewRenderer, ToggleStateHolder by toggleStateHolder {
@ -23,7 +23,6 @@ class DefaultBlockViewRenderer(
focus: Id,
anchor: Id,
indent: Int,
counter: Counter,
details: Block.Details
): List<BlockView> {
@ -87,7 +86,6 @@ class DefaultBlockViewRenderer(
indent = indent.inc(),
anchor = block.id,
root = root,
counter = counter,
details = details
)
)
@ -139,13 +137,26 @@ class DefaultBlockViewRenderer(
counter.reset()
result.add(file(content, block, indent))
}
is Content.Layout -> {
counter.reset()
result.addAll(
render(
mode = mode,
focus = focus,
indent = indent,
anchor = block.id,
root = root,
details = details
)
)
}
}
}
return result
}
private suspend fun buildTitle(
private fun buildTitle(
mode: EditorMode,
anchor: Id,
root: Block,
@ -162,7 +173,7 @@ class DefaultBlockViewRenderer(
text = details.details[root.id]?.name,
emoji = details.details[root.id]?.icon?.let { name ->
if (name.isNotEmpty())
emojifier.fromShortName(name).unicode
name
else
null
}
@ -384,7 +395,7 @@ class DefaultBlockViewRenderer(
else -> throw IllegalStateException("Unexpected file type: ${content.type}")
}
private suspend fun title(
private fun title(
mode: EditorMode,
block: Block,
content: Content.Text,
@ -396,14 +407,14 @@ class DefaultBlockViewRenderer(
text = content.text,
emoji = root.fields.icon?.let { name ->
if (name.isNotEmpty())
emojifier.fromShortName(name).unicode
name
else
null
},
focused = block.id == focus
)
private suspend fun page(
private fun page(
block: Block,
content: Content.Link,
indent: Int,
@ -413,7 +424,7 @@ class DefaultBlockViewRenderer(
isEmpty = true,
emoji = details.details[content.target]?.icon?.let { name ->
if (name.isNotEmpty())
emojifier.fromShortName(name).unicode
name
else
null
},

View file

@ -46,13 +46,11 @@ class HomeDashboardViewMapperTest {
blocks = listOf(child),
children = listOf(child.id),
fields = Block.Fields.empty(),
type = Block.Content.Dashboard.Type.MAIN_SCREEN
type = Block.Content.Smart.Type.HOME
)
val view = runBlocking {
dashboard.toView(
emojifier = emojifier
)
dashboard.toView()
}
assertEquals(
@ -85,14 +83,14 @@ class HomeDashboardViewMapperTest {
blocks = listOf(child),
children = listOf(child.id),
fields = Block.Fields.empty(),
type = Block.Content.Dashboard.Type.MAIN_SCREEN
type = Block.Content.Smart.Type.HOME
)
emojifier.stub {
onBlocking { fromShortName(any()) } doReturn emoji
}
val view: List<DashboardView> = runBlocking { dashboard.toView(emojifier = emojifier) }
val view: List<DashboardView> = runBlocking { dashboard.toView() }
assertEquals(
expected = listOf(

View file

@ -20,8 +20,10 @@ import com.agileburo.anytype.domain.emoji.Emoji
import com.agileburo.anytype.domain.emoji.Emojifier
import com.agileburo.anytype.domain.event.interactor.InterceptEvents
import com.agileburo.anytype.domain.event.model.Event
import com.agileburo.anytype.domain.event.model.Payload
import com.agileburo.anytype.domain.image.LoadImage
import com.agileburo.anytype.domain.page.CreatePage
import com.agileburo.anytype.presentation.desktop.HomeDashboardEventConverter
import com.agileburo.anytype.presentation.desktop.HomeDashboardStateMachine
import com.agileburo.anytype.presentation.desktop.HomeDashboardViewModel
import com.agileburo.anytype.presentation.mapper.toView
@ -35,6 +37,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@ -74,9 +77,6 @@ class HomeDashboardViewModelTest {
@Mock
lateinit var dnd: DragAndDrop
@Mock
lateinit var emojifier: Emojifier
private lateinit var vm: HomeDashboardViewModel
@Before
@ -93,20 +93,28 @@ class HomeDashboardViewModelTest {
createPage = createPage,
getConfig = getConfig,
dragAndDrop = dnd,
interceptEvents = interceptEvents
interceptEvents = interceptEvents,
eventConverter = HomeDashboardEventConverter.DefaultConverter()
)
}
@Test
fun `should only start getting config when view model is initialized`() {
val config =
Config(home = MockDataFactory.randomUuid(), gateway = MockDataFactory.randomUuid())
// SETUP
val config = Config(
home = MockDataFactory.randomUuid(),
gateway = MockDataFactory.randomUuid()
)
val response = Either.Right(config)
stubGetConfig(response)
stubObserveEvents(params = InterceptEvents.Params(context = null))
// TESTING
vm = buildViewModel()
verify(getConfig, times(1)).invoke(any(), any(), any())
@ -118,15 +126,22 @@ class HomeDashboardViewModelTest {
@Test
fun `should start observing events after receiving config`() {
val config =
Config(home = MockDataFactory.randomUuid(), gateway = MockDataFactory.randomUuid())
// SETUP
val config = Config(
home = MockDataFactory.randomUuid(),
gateway = MockDataFactory.randomUuid()
)
val response = Either.Right(config)
val params = InterceptEvents.Params(context = null)
val params = InterceptEvents.Params(context = config.home)
stubGetConfig(response)
stubObserveEvents(params = params)
// TESTING
vm = buildViewModel()
verify(getConfig, times(1)).invoke(any(), any(), any())
@ -136,12 +151,19 @@ class HomeDashboardViewModelTest {
@Test
fun `should start observing home dashboard after receiving config`() {
val config =
Config(home = MockDataFactory.randomUuid(), gateway = MockDataFactory.randomUuid())
// SETUP
val config = Config(
home = MockDataFactory.randomUuid(),
gateway = MockDataFactory.randomUuid()
)
val response = Either.Right(config)
stubGetConfig(response)
stubObserveEvents(params = InterceptEvents.Params(context = null))
stubObserveEvents(params = InterceptEvents.Params(context = config.home))
// TESTING
vm = buildViewModel()
@ -154,11 +176,18 @@ class HomeDashboardViewModelTest {
@Test
fun `should emit loading state when home dashboard loading started`() {
val config =
Config(home = MockDataFactory.randomUuid(), gateway = MockDataFactory.randomUuid())
// SETUP
val config = Config(
home = MockDataFactory.randomUuid(),
gateway = MockDataFactory.randomUuid()
)
stubGetConfig(Either.Right(config))
stubObserveEvents(params = InterceptEvents.Params(context = null))
stubObserveEvents(params = InterceptEvents.Params(context = config.home))
stubOpenDashboard()
// TESTING
vm = buildViewModel()
@ -177,8 +206,12 @@ class HomeDashboardViewModelTest {
@Test
fun `should emit view state with dashboard when home dashboard loading started`() {
val config =
Config(home = MockDataFactory.randomUuid(), gateway = MockDataFactory.randomUuid())
// SETUP
val config = Config(
home = MockDataFactory.randomUuid(),
gateway = MockDataFactory.randomUuid()
)
val page = Block(
id = MockDataFactory.randomUuid(),
@ -191,19 +224,21 @@ class HomeDashboardViewModelTest {
val dashboard = Block(
id = config.home,
content = Block.Content.Dashboard(
type = Block.Content.Dashboard.Type.MAIN_SCREEN
content = Block.Content.Smart(
type = Block.Content.Smart.Type.HOME
),
children = listOf(page.id),
fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString()))
)
val delayInMillis = 100L
stubGetConfig(Either.Right(config))
val events = flow {
delay(delayInMillis)
emit(
listOf(
stubObserveEvents(params = InterceptEvents.Params(context = config.home))
stubOpenDashboard(
payload = Payload(
context = config.home,
events = listOf(
Event.Command.ShowBlock(
root = config.home,
context = config.home,
@ -211,30 +246,14 @@ class HomeDashboardViewModelTest {
)
)
)
}
stubGetConfig(Either.Right(config))
stubObserveEvents(
params = InterceptEvents.Params(context = null),
flow = events
)
stubOpenDashboard()
// TESTING
vm = buildViewModel()
vm.onViewCreated()
vm.state.test().assertValue(
HomeDashboardStateMachine.State(
isLoading = true,
isInitialzed = true,
dashboard = null,
error = null
)
)
coroutineTestRule.advanceTime(delayInMillis)
vm.state.test().assertValue(
HomeDashboardStateMachine.State(
isLoading = false,
@ -248,6 +267,8 @@ class HomeDashboardViewModelTest {
@Test
fun `block dragging events do not alter overall state`() {
// SETUP
val config = Config(
home = MockDataFactory.randomUuid(),
gateway = MockDataFactory.randomUuid()
@ -293,11 +314,10 @@ class HomeDashboardViewModelTest {
val dashboard = Block(
id = config.home,
content = Block.Content.Dashboard(
type = Block.Content.Dashboard.Type.MAIN_SCREEN
content = Block.Content.Smart(
type = Block.Content.Smart.Type.HOME
),
children = pages.map { page -> page.id },
fields = Block.Fields.empty()
)
@ -316,16 +336,23 @@ class HomeDashboardViewModelTest {
)
}
emojifier.stub {
onBlocking { fromShortName(any()) } doReturn emoji
}
stubGetConfig(
Either.Right(config)
)
stubGetConfig(Either.Right(config))
stubObserveEvents(
params = InterceptEvents.Params(context = null),
params = InterceptEvents.Params(context = config.home),
flow = events
)
stubOpenDashboard()
stubOpenDashboard(
payload = Payload(
context = config.home,
events = emptyList()
)
)
// TESTING
vm = buildViewModel()
@ -349,9 +376,7 @@ class HomeDashboardViewModelTest {
dashboard,
pages.first(),
pages.last()
).toHomeDashboard(dashboard.id).toView(
emojifier = emojifier
)
).toHomeDashboard(dashboard.id).toView()
}
val from = 0
@ -418,24 +443,15 @@ class HomeDashboardViewModelTest {
val dashboard = HomeDashboard(
id = config.home,
fields = Block.Fields(map = mapOf("name" to MockDataFactory.randomString())),
type = Block.Content.Dashboard.Type.MAIN_SCREEN,
type = Block.Content.Smart.Type.HOME,
blocks = pages,
children = pages.map { it.id }
)
val delayInMillis = 100L
val flow = flow {
delay(delayInMillis)
emit(dashboard)
}
emojifier.stub {
onBlocking { fromShortName(any()) } doReturn emoji
}
stubGetConfig(Either.Right(config))
stubObserveEvents(params = InterceptEvents.Params(context = null))
stubObserveEvents(params = InterceptEvents.Params(context = config.home))
stubOpenDashboard()
vm = buildViewModel()
@ -445,9 +461,7 @@ class HomeDashboardViewModelTest {
coroutineTestRule.advanceTime(delayInMillis)
val views = runBlocking {
dashboard.toView(
emojifier = emojifier
)
dashboard.toView()
}
val from = 0
@ -479,16 +493,27 @@ class HomeDashboardViewModelTest {
@Test
fun `should proceed with getting account and opening dashboard when view is created`() {
val config =
Config(home = MockDataFactory.randomUuid(), gateway = MockDataFactory.randomUuid())
// SETUP
val config = Config(
home = MockDataFactory.randomUuid(),
gateway = MockDataFactory.randomUuid()
)
stubGetConfig(Either.Right(config))
stubObserveEvents(params = InterceptEvents.Params(context = null))
stubObserveEvents(params = InterceptEvents.Params(context = config.home))
stubOpenDashboard()
// TESTING
vm = buildViewModel()
vm.onViewCreated()
verify(getCurrentAccount, times(1)).invoke(any(), any(), any())
verify(openDashboard, times(1)).invoke(any(), eq(null), any())
runBlockingTest {
verify(openDashboard, times(1)).invoke(eq(null))
}
}
@Test
@ -504,6 +529,8 @@ class HomeDashboardViewModelTest {
val response = Either.Right(account)
stubGetCurrentAccount(response)
stubObserveEvents()
stubOpenDashboard()
vm = buildViewModel()
@ -533,8 +560,8 @@ class HomeDashboardViewModelTest {
val imageResponse = Either.Right(blob)
stubObserveEvents()
stubGetCurrentAccount(accountResponse)
stubOpenDashboard()
loadImage.stub {
onBlocking { invoke(any(), any(), any()) } doAnswer { answer ->
@ -568,6 +595,7 @@ class HomeDashboardViewModelTest {
stubObserveEvents()
stubGetCurrentAccount(accountResponse)
stubOpenDashboard()
vm = buildViewModel()
@ -595,18 +623,8 @@ class HomeDashboardViewModelTest {
val id = MockDataFactory.randomUuid()
stubObserveEvents()
closeDashboard.stub {
onBlocking { invoke(any(), any(), any()) } doAnswer { answer ->
answer.getArgument<(Either<Throwable, Unit>) -> Unit>(2)(Either.Right(Unit))
}
}
createPage.stub {
onBlocking { invoke(any(), any(), any()) } doAnswer { answer ->
answer.getArgument<(Either<Throwable, String>) -> Unit>(2)(Either.Right(id))
}
}
stubCloseDashboard()
stubCreatePage(id)
vm = buildViewModel()
@ -647,8 +665,8 @@ class HomeDashboardViewModelTest {
val dashboard = Block(
id = config.home,
content = Block.Content.Dashboard(
type = Block.Content.Dashboard.Type.MAIN_SCREEN
content = Block.Content.Smart(
type = Block.Content.Smart.Type.HOME
),
children = listOf(page.id),
fields = Block.Fields(map = mapOf("name" to dashboardName))
@ -676,10 +694,12 @@ class HomeDashboardViewModelTest {
}
stubGetConfig(Either.Right(config))
stubObserveEvents(
flow = events,
params = InterceptEvents.Params(context = null)
params = InterceptEvents.Params(context = config.home)
)
stubOpenDashboard()
vm = buildViewModel()
@ -691,7 +711,7 @@ class HomeDashboardViewModelTest {
val firstExpectedState = HomeDashboard(
id = config.home,
fields = Block.Fields(map = mapOf("name" to dashboardName)),
type = Block.Content.Dashboard.Type.MAIN_SCREEN,
type = Block.Content.Smart.Type.HOME,
blocks = listOf(page),
children = listOf(page.id)
)
@ -699,7 +719,7 @@ class HomeDashboardViewModelTest {
val secondExpectedState = HomeDashboard(
id = config.home,
fields = Block.Fields(map = mapOf("name" to dashboardName)),
type = Block.Content.Dashboard.Type.MAIN_SCREEN,
type = Block.Content.Smart.Type.HOME,
blocks = listOf(page, new),
children = listOf(page.id)
)
@ -742,8 +762,27 @@ class HomeDashboardViewModelTest {
}
}
private fun stubOpenDashboard() {
private fun stubOpenDashboard(
payload: Payload = Payload(
context = MockDataFactory.randomString(),
events = emptyList()
)
) {
openDashboard.stub {
onBlocking { invoke(params = null) } doReturn Either.Right(payload)
}
}
private fun stubCreatePage(id: String) {
createPage.stub {
onBlocking { invoke(any(), any(), any()) } doAnswer { answer ->
answer.getArgument<(Either<Throwable, String>) -> Unit>(2)(Either.Right(id))
}
}
}
private fun stubCloseDashboard() {
closeDashboard.stub {
onBlocking { invoke(any(), any(), any()) } doAnswer { answer ->
answer.getArgument<(Either<Throwable, Unit>) -> Unit>(2)(Either.Right(Unit))
}

View file

@ -14,9 +14,6 @@ import kotlin.test.assertTrue
class HomeDashboardViewMapperTest {
@Mock
lateinit var emojifier: Emojifier
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
@ -54,7 +51,7 @@ class HomeDashboardViewMapperTest {
blocks = listOf(archived, active),
children = listOf(archived.id, active.id),
fields = Block.Fields.empty(),
type = Block.Content.Dashboard.Type.MAIN_SCREEN,
type = Block.Content.Smart.Type.HOME,
details = Block.Details(
details = mapOf(
target to Block.Fields(
@ -67,9 +64,7 @@ class HomeDashboardViewMapperTest {
)
val result = runBlocking {
dashboard.toView(
emojifier = emojifier
)
dashboard.toView()
}
assertTrue {

View file

@ -31,13 +31,11 @@ class DefaultBlockViewRendererTest {
suspend fun render(
root: Block,
anchor: Id,
counter: Counter = Counter.Default(),
focus: Id,
indent: Int
): List<BlockView> = blocks.render(
root = root,
anchor = anchor,
counter = counter,
focus = focus,
indent = indent
)
@ -63,8 +61,8 @@ class DefaultBlockViewRendererTest {
MockitoAnnotations.initMocks(this)
renderer = DefaultBlockViewRenderer(
urlBuilder = UrlBuilder(config),
emojifier = emojifier,
toggleStateHolder = toggleStateHolder
toggleStateHolder = toggleStateHolder,
counter = Counter.Default()
)
}

View file

@ -3,6 +3,8 @@ package anytype;
option go_package = "pb";
import "models.proto";
import "localstore.proto";
import "events.proto";
import "google/protobuf/struct.proto";
/*
@ -24,7 +26,7 @@ message Rpc {
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
string description = 2;
@ -66,6 +68,30 @@ message Rpc {
message BlockList {
message ConvertChildrenToPages {
message Request {
string contextId = 1;
repeated string blockIds = 2;
}
message Response {
Error error = 1;
repeated string linkIds = 2;
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0;
UNKNOWN_ERROR = 1;
BAD_INPUT = 2;
// ...
}
}
}
}
message Move {
message Request {
string contextId = 1;
@ -77,6 +103,7 @@ message Rpc {
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
@ -96,14 +123,15 @@ message Rpc {
message Request {
string contextId = 1;
repeated string blockIds = 2;
anytype.model.Block block = 3;
string dropTargetId = 4; // id of the block in Page(contextId) to create a link near that block
google.protobuf.Struct details = 3; // page details
string dropTargetId = 4;
anytype.model.Block.Position position = 5;
}
message Response {
Error error = 1;
string linkId = 2;
ResponseEvent event = 3;
message Error {
Code code = 1;
@ -133,6 +161,7 @@ message Rpc {
message Response {
Error error = 1;
repeated string blockIds = 2;
ResponseEvent event = 3;
message Error {
Code code = 1;
@ -149,6 +178,32 @@ message Rpc {
}
message Set {
message Page {
message IsArchived {
message Request {
string contextId = 1;
repeated string blockIds = 2;
bool isArchived = 3;
}
message Response {
Error error = 1;
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0;
UNKNOWN_ERROR = 1;
BAD_INPUT = 2;
// ...
}
}
}
}
}
message Text {
message Style {
message Request {
@ -159,6 +214,7 @@ message Rpc {
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
@ -182,6 +238,7 @@ message Rpc {
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
@ -207,6 +264,7 @@ message Rpc {
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
@ -230,6 +288,7 @@ message Rpc {
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
@ -256,6 +315,60 @@ message Rpc {
}
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0;
UNKNOWN_ERROR = 1;
BAD_INPUT = 2;
// ...
}
}
}
}
message Div {
message Style {
message Request {
string contextId = 1;
repeated string blockIds = 2;
anytype.model.Block.Content.Div.Style style = 3;
}
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0;
UNKNOWN_ERROR = 1;
BAD_INPUT = 2;
// ...
}
}
}
}
}
}
message Delete {
// Deletes the page, keys and all records from the local store and unsubscribe from remote changes
message Page {
message Request {
repeated string blockIds = 1; // pages to remove
}
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
@ -287,6 +400,7 @@ message Rpc {
message Response {
Error error = 1;
string blockId = 2;
ResponseEvent event = 3;
message Error {
Code code = 1;
@ -302,18 +416,18 @@ message Rpc {
}
}
message Split {
message Request {
string contextId = 1;
string blockId = 2;
int32 cursorPosition = 3;
anytype.model.Range range = 3;
anytype.model.Block.Content.Text.Style style = 4;
}
message Response {
Error error = 1;
string blockId = 2;
ResponseEvent event = 3;
message Error {
Code code = 1;
@ -338,6 +452,7 @@ message Rpc {
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
@ -358,6 +473,7 @@ message Rpc {
message Request {
string contextId = 1;
repeated anytype.model.Block blocks = 2;
anytype.model.Range selectedTextRange = 3;
}
message Response {
@ -384,15 +500,18 @@ message Rpc {
string focusedBlockId = 2;
anytype.model.Range selectedTextRange = 3;
repeated string selectedBlockIds = 4;
anytype.model.Range copyTextRange = 5;
string textSlot = 5;
string htmlSlot = 6;
repeated anytype.model.Block anySlot = 7;
string textSlot = 6;
string htmlSlot = 7;
repeated anytype.model.Block anySlot = 8;
}
message Response {
Error error = 1;
repeated string blockIds = 2;
int32 caretPosition = 3;
ResponseEvent event = 4;
message Error {
Code code = 1;
@ -412,6 +531,7 @@ message Rpc {
message Request {
string contextId = 1;
repeated anytype.model.Block blocks = 2;
anytype.model.Range selectedTextRange = 3;
}
message Response {
@ -419,6 +539,7 @@ message Rpc {
string textSlot = 2;
string htmlSlot = 3;
repeated anytype.model.Block anySlot = 4;
ResponseEvent event = 5;
message Error {
Code code = 1;
@ -443,6 +564,7 @@ message Rpc {
message Response {
Error error = 1;
string path = 2;
ResponseEvent event = 3;
message Error {
Code code = 1;
@ -468,6 +590,7 @@ message Rpc {
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
@ -491,6 +614,7 @@ message Rpc {
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
@ -516,6 +640,7 @@ message Rpc {
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
@ -564,6 +689,7 @@ message Rpc {
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
@ -640,6 +766,7 @@ message Rpc {
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
@ -665,6 +792,7 @@ message Rpc {
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
@ -689,6 +817,7 @@ message Rpc {
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
@ -715,6 +844,7 @@ message Rpc {
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
@ -841,6 +971,7 @@ message Rpc {
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
@ -858,6 +989,7 @@ message Rpc {
}
}
message Bookmark {
message Fetch {
message Request {
@ -868,6 +1000,63 @@ message Rpc {
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0;
UNKNOWN_ERROR = 1;
BAD_INPUT = 2;
}
}
}
}
message CreateAndFetch {
message Request {
string contextId = 1;
string targetId = 2;
anytype.model.Block.Position position = 3;
string url = 4;
}
message Response {
Error error = 1;
string blockId = 2;
ResponseEvent event = 3;
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0;
UNKNOWN_ERROR = 1;
BAD_INPUT = 2;
}
}
}
}
}
message File {
message CreateAndUpload {
message Request {
string contextId = 1;
string targetId = 2;
anytype.model.Block.Position position = 3;
string url = 4;
string localPath = 5;
anytype.model.Block.Content.File.Type fileType = 6;
}
message Response {
Error error = 1;
string blockId = 2;
ResponseEvent event = 3;
message Error {
Code code = 1;
@ -896,6 +1085,7 @@ message Rpc {
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
@ -919,6 +1109,7 @@ message Rpc {
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
@ -942,6 +1133,7 @@ message Rpc {
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
@ -979,12 +1171,35 @@ message Rpc {
message Request {
string contextId = 1; // id of the context blo1k
string blockId = 2;
repeated string breadcrumbsIds = 3; // optional ids of breadcrubms blocks
}
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0;
UNKNOWN_ERROR = 1;
BAD_INPUT = 2;
// ...
}
}
}
}
message GetPublicWebURL {
message Request {
string blockId = 1;
}
message Response {
Error error = 1;
string url = 2;
message Error {
Code code = 1;
string description = 2;
@ -1007,6 +1222,7 @@ message Rpc {
message Response {
Error error = 1;
string blockId = 2;
ResponseEvent event = 3;
message Error {
Code code = 1;
@ -1022,14 +1238,15 @@ message Rpc {
}
}
message CutBreadcrumbs {
message SetBreadcrumbs {
message Request {
string breadcrumbsId = 1;
int32 index = 2; // 0 - for full reset
repeated string ids = 2; // page ids
}
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
@ -1071,6 +1288,7 @@ message Rpc {
message Response {
Error error = 1;
string blockId = 2;
ResponseEvent event = 3;
message Error {
Code code = 1;
@ -1098,6 +1316,7 @@ message Rpc {
Error error = 1;
string blockId = 2;
string targetId = 3;
ResponseEvent event = 4;
message Error {
Code code = 1;
@ -1112,6 +1331,7 @@ message Rpc {
}
}
}
/*
* Remove blocks from the childrenIds of its parents
*/
@ -1124,6 +1344,7 @@ message Rpc {
message Response {
Error error = 1;
ResponseEvent event = 2;
message Error {
Code code = 1;
@ -1246,6 +1467,9 @@ message Rpc {
string avatarLocalPath = 2; // Path to an image, that will be used as an avatar of this account
string avatarColor = 3; // Avatar color as an alternative for avatar image
}
string alphaInviteCode = 20;
}
/**
@ -1269,6 +1493,7 @@ message Rpc {
ACCOUNT_CREATED_BUT_FAILED_TO_SET_AVATAR = 103;
FAILED_TO_STOP_RUNNING_NODE = 104;
BAD_INVITE_CODE = 900;
}
}
}
@ -1544,6 +1769,27 @@ message Rpc {
}
}
message Shutdown {
message Request {
}
message Response {
Error error = 1;
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0;
UNKNOWN_ERROR = 1;
BAD_INPUT = 2;
NODE_NOT_STARTED = 101;
}
}
}
}
message Config {
message Get {
message Request {
@ -1551,7 +1797,8 @@ message Rpc {
message Response {
Error error = 1;
string homeBlockId = 2; // home dashboard block id
string archiveBlockId = 3; // home dashboard block id
string archiveBlockId = 3; // archive block id
string profileBlockId = 4; // profile block id
string gatewayUrl = 101; // gateway url for fetching static files
message Error {
Code code = 1;
@ -1564,7 +1811,6 @@ message Rpc {
NODE_NOT_STARTED = 101;
}
}
}
}
}
@ -1643,6 +1889,7 @@ message Rpc {
string url = 1;
string localPath = 2;
anytype.model.Block.Content.File.Type type = 3;
bool disableEncryption = 4;
}
message Response {
@ -1662,6 +1909,55 @@ message Rpc {
}
}
message Navigation {
message ListPages {
message Request {
}
message Response {
Error error = 1;
repeated anytype.model.PageInfo pages = 2;
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0;
UNKNOWN_ERROR = 1;
BAD_INPUT = 2;
// ...
}
}
}
}
/*
* Get the info for page alongside with info for all inbound and outbound links from/to this page
*/
message GetPageInfoWithLinks {
message Request {
string pageId = 1;
}
message Response {
Error error = 1;
anytype.model.PageInfoWithLinks page = 2;
message Error {
Code code = 1;
string description = 2;
enum Code {
NULL = 0;
UNKNOWN_ERROR = 1;
BAD_INPUT = 2;
// ...
}
}
}
}
}
}
message Empty {

View file

@ -16,6 +16,7 @@ message Event {
message Message {
oneof value {
Account.Show accountShow = 1;
Account.Details accountDetails = 201;
Block.Add blockAdd = 2;
Block.Delete blockDelete = 3;
@ -33,6 +34,7 @@ message Event {
Block.Set.Bookmark blockSetBookmark = 14;
Block.Set.Align blockSetAlign = 15;
Block.Set.Details blockSetDetails = 16;
Block.Set.Div blockSetDiv = 17;
Block.Show blockShow = 20;
User.Block.Join userBlockJoin = 21;
@ -56,6 +58,11 @@ message Event {
int32 index = 1; // Number of an account in an all found accounts list
anytype.model.Account account = 2; // An Account, that has been found for the mnemonic
}
message Details {
string profileId = 1;
google.protobuf.Struct details = 2;
}
}
message Block {
/*
@ -75,13 +82,14 @@ message Event {
}
/*
* Works with a smart blocks: Page, Dashboard
* Dashboard opened, click on a page, Rpc.Block.open, Block.ShowFullscreen(PageBlock)
* Works with a smart blocks: Page, Dashboard
* Dashboard opened, click on a page, Rpc.Block.open, Block.ShowFullscreen(PageBlock)
*/
message Show {
string rootId = 1; // Root block id
repeated anytype.model.Block blocks = 2; // dependent blocks (descendants)
repeated Block.Set.Details details = 3; // details for current and dependent smart blocks
SmartBlockType type = 4;
}
/**
@ -169,6 +177,16 @@ message Event {
}
message Div {
string id = 1;
Style style = 2;
message Style {
anytype.model.Block.Content.Div.Style value = 1;
}
}
message File {
string id = 1;
Type type = 2;
@ -331,6 +349,19 @@ message Event {
}
enum SmartBlockType {
Page = 0;
Home = 1;
ProfilePage = 2;
Archive = 3;
Breadcrumbs = 4;
}
message ResponseEvent {
repeated Event.Message messages = 1;
string contextId = 2;
}
message Model {
message Process {
string id = 1;
@ -355,4 +386,4 @@ message Model {
int64 done = 2;
}
}
}
}

View file

@ -0,0 +1,53 @@
syntax = "proto3";
package anytype.model;
option go_package = "github.com/anytypeio/go-anytype-library/pb/model";
import "google/protobuf/struct.proto";
message State {
map<string, uint64> state = 1;
}
message PageInfo {
string id = 1;
google.protobuf.Struct details = 2;
string snippet = 3;
State state = 4;
int64 lastOpened = 5;
uint32 inboundLinksCount = 6;
}
message PageDetails {
google.protobuf.Struct details = 1;
}
message PageLinks {
repeated string inboundIDs = 1;
repeated string outboundIDs = 2;
}
message PageLinksInfo {
repeated PageInfo inbound = 1;
repeated PageInfo outbound = 2;
}
message PageInfoWithLinks {
string id = 1;
PageInfo info = 2;
PageLinksInfo links = 3;
State state = 4;
}
message PageInfoWithOutboundLinks {
string id = 1;
PageInfo info = 2;
repeated PageInfo outboundLinks = 3;
State state = 4;
}
message PageInfoWithOutboundLinksIDs {
string id = 1;
PageInfo info = 2;
repeated string outboundLinks = 3;
State state = 4;
}

View file

@ -4,16 +4,10 @@ option go_package = "github.com/anytypeio/go-anytype-library/pb/model";
import "google/protobuf/struct.proto";
message SmartBlock {
string id = 1;
Type type = 2;
enum Type {
Dashboard = 0;
Page = 1;
Archive = 2;
Breadcrumbs = 3;
Dataview = 4;
}
message SmartBlockSnapshotBase {
repeated Block blocks = 1;
google.protobuf.Struct details = 2;
google.protobuf.Struct fileKeys = 3;
}
message Block {
@ -25,9 +19,7 @@ message Block {
Align align = 6;
oneof content {
Content.Dashboard dashboard = 11;
Content.Page page = 12;
Content.Dataview dataview = 13;
Content.Smartblock smartblock = 11;
Content.Text text = 14;
Content.File file = 15;
@ -36,6 +28,7 @@ message Block {
Content.Bookmark bookmark = 18;
Content.Icon icon = 19;
Content.Link link = 20;
Content.Dataview dataview = 21;
}
@ -74,6 +67,7 @@ message Block {
enum Style {
Row = 0;
Column = 1;
Div = 2;
}
}
@ -122,22 +116,6 @@ message Block {
string name = 1;
}
/*
* Block type to organize pages on the main screen (main purpose)
* It also can be mounted on a page.
*/
message Dashboard {
enum Style {
MainScreen = 0;
Archive = 1;
}
Style style = 1;
}
message Dataview {
}
message Text {
string text = 1;
Style style = 2;
@ -207,19 +185,11 @@ message Block {
}
}
message Page {
enum Style {
Empty = 0; // Ordinary page, without additional fields
Task = 1; // Page with a task fields
Set = 2; // Page, that organize a set of blocks by a specific criterio
Profile = 3;
message Smartblock {
}
Breadcrumbs = 101;
// ...
}
Style style = 1;
message Dataview {
string databaseId = 1;
}
}
}

View file

@ -1,5 +1,4 @@
include ':app', ':library-emojifier',
':sample',
include ':app',
':core-utils',
':middleware',
':protobuf',
@ -8,6 +7,9 @@ include ':app', ':library-emojifier',
':data',
':device',
':presentation',
':presentation-editor',
':core-ui',
':library-kanban-widget',
':library-page-icon-picker-widget'
':library-page-icon-picker-widget',
':library-emojifier',
':sample'