Link presenters init (#1579)

Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
Denis Bykhov 2022-04-28 16:05:28 +06:00 committed by GitHub
parent 83e8963e38
commit d2f2a18376
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 354 additions and 8 deletions

View File

@ -27,6 +27,7 @@ import type {
HTMLPresenter,
IgnoreActions,
KeyBinding,
LinkPresenter,
ObjectEditor,
ObjectEditorHeader,
ObjectFactory,
@ -199,6 +200,13 @@ export class TPreviewPresenter extends TClass implements PreviewPresenter {
presenter!: AnyComponent
}
@Model(view.class.LinkPresenter, core.class.Doc, DOMAIN_MODEL)
export class TLinkPresenter extends TDoc implements LinkPresenter {
pattern!: string
component!: AnyComponent
}
export function createModel (builder: Builder): void {
builder.createModel(
TAttributeEditor,
@ -216,7 +224,8 @@ export function createModel (builder: Builder): void {
TSpaceName,
TTextPresenter,
TIgnoreActions,
TPreviewPresenter
TPreviewPresenter,
TLinkPresenter
)
classPresenter(builder, core.class.TypeString, view.component.StringPresenter, view.component.StringEditor)
@ -308,6 +317,16 @@ export function createModel (builder: Builder): void {
singleInput: true
})
builder.createDoc(view.class.LinkPresenter, core.space.Model, {
pattern: '(www.)?youtube.(com|ru)',
component: view.component.YoutubePresenter
})
builder.createDoc(view.class.LinkPresenter, core.space.Model, {
pattern: '(www.)?github.com/',
component: view.component.GithubPresenter
})
// Should be contributed via individual plugins.
// actionTarget(builder, view.action.Open, core.class.Doc, { mode: ['browser', 'context'] })
}

View File

@ -71,7 +71,9 @@ export default mergeIds(viewId, view, {
DateEditor: '' as AnyComponent,
DatePresenter: '' as AnyComponent,
TableView: '' as AnyComponent,
RolePresenter: '' as AnyComponent
RolePresenter: '' as AnyComponent,
YoutubePresenter: '' as AnyComponent,
GithubPresenter: '' as AnyComponent
},
string: {
Table: '' as IntlString,

View File

@ -42,6 +42,8 @@ export let nodes: NodeListOf<any>
<br/>
{:else if node.nodeName === 'HR'}
<hr/>
{:else if node.nodeName === 'IMG'}
<div class="max-h-60 max-w-60 img">{@html node.outerHTML}</div>
{:else if node.nodeName === 'H1'}
<h1><svelte:self nodes={node.childNodes}/></h1>
{:else if node.nodeName === 'H2'}
@ -70,3 +72,13 @@ export let nodes: NodeListOf<any>
{/if}
{/each}
{/if}
<style lang="scss">
.img {
:global(img) {
object-fit: contain;
height: 100%;
width: 100%
}
}
</style>

View File

@ -437,6 +437,8 @@ p:last-child { margin-block-end: 0; }
.min-w-min { min-width: min-content; }
.min-h-0 { min-height: 0; }
.max-h-125 { max-height: 31.25rem; }
.max-h-60 { max-height: 15rem; }
.max-w-60 { max-width: 15rem; }
.clear-mins {
min-width: 0;
min-height: 0;

View File

@ -23,7 +23,7 @@
import { Avatar, getClient, MessageViewer } from '@anticrm/presentation'
import ui, { ActionIcon, IconMoreH, Menu, showPopup, Label, Tooltip, Button } from '@anticrm/ui'
import { Action } from '@anticrm/view'
import { getActions } from '@anticrm/view-resources'
import { getActions, LinkPresenter } from '@anticrm/view-resources'
import { createEventDispatcher } from 'svelte'
import { AddMessageToSaved, DeleteMessageFromSaved, UnpinMessage } from '../index'
import chunter from '../plugin'
@ -152,6 +152,27 @@
$: parentMessage = message as Message
$: hasReplies = (parentMessage?.replies?.length ?? 0) > 0
$: links = getLinks(message.content)
function getLinks (content: string): HTMLLinkElement[] {
const parser = new DOMParser()
const parent = parser.parseFromString(content, 'text/html').firstChild?.childNodes[1] as HTMLElement
return parseLinks(parent.childNodes)
}
function parseLinks (nodes: NodeListOf<ChildNode>): HTMLLinkElement[] {
const res: HTMLLinkElement[] = []
nodes.forEach((p) => {
if (p.nodeType !== Node.TEXT_NODE) {
if (p.nodeName === 'A') {
res.push(p as HTMLLinkElement)
}
res.push(...parseLinks(p.childNodes))
}
})
return res
}
</script>
<div class="container">
@ -189,6 +210,9 @@
<AttachmentList {attachments} {savedAttachmentsIds} />
</div>
{/if}
{#each links as link}
<LinkPresenter {link}/>
{/each}
{/if}
{#if reactions || (!thread && hasReplies)}
<div class="footer flex-col">

View File

@ -13,6 +13,8 @@
"Role": "Role",
"DeleteObject": "Delete object",
"DeleteObjectConfirm": "Do you want to delete this {count, plural, =1 {object} other {# objects}}?",
"Open": "Open"
"Open": "Open",
"Assignees": "Assignees",
"Labels": "Labels"
}
}

View File

@ -13,6 +13,8 @@
"Role": "Роль",
"DeleteObject": "Удалить объект",
"DeleteObjectConfirm": "Вы действительно хотите удалить {count, plural, =1 {этот обьект} other {эти # обьекта}}?",
"Open": "Открыть"
"Open": "Открыть",
"Assignees": "Исполнители",
"Labels": "Метки"
}
}

View File

@ -0,0 +1,40 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { getClient } from "@anticrm/presentation"
import { AnyComponent, Component } from "@anticrm/ui"
import view from '../plugin'
export let link: HTMLLinkElement
const client = getClient()
async function getPresenter (href: string): Promise<AnyComponent | undefined> {
const presenters = await client.findAll(view.class.LinkPresenter, {
})
for (const presenter of presenters) {
if (new RegExp(presenter.pattern).test(href)) {
return presenter.component
}
}
return
}
</script>
{#await getPresenter(link.href) then presenter}
{#if presenter}
<Component is={presenter} props={{ href: link.href }} />
{/if}
{/await}

View File

@ -0,0 +1,26 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
export let size: 'small' | 'medium' | 'large' | 'full'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<g {fill}>
<path d="M31.356,25.677l38.625,22.3c1.557,0.899,1.557,3.147,0,4.046l-38.625,22.3c-1.557,0.899-3.504-0.225-3.504-2.023V27.7 C27.852,25.902,29.798,24.778,31.356,25.677z"/>
<path d="M69.981,47.977l-38.625-22.3c-0.233-0.134-0.474-0.21-0.716-0.259l37.341,21.559c1.557,0.899,1.557,3.147,0,4.046 l-38.625,22.3c-0.349,0.201-0.716,0.288-1.078,0.301c0.656,0.938,1.961,1.343,3.078,0.699l38.625-22.3 C71.538,51.124,71.538,48.876,69.981,47.977z"/>
<path d="M31.356,25.677l38.625,22.3c1.557,0.899,1.557,3.147,0,4.046 l-38.625,22.3c-1.557,0.899-3.504-0.225-3.504-2.023V27.7C27.852,25.902,29.798,24.778,31.356,25.677z" style="fill:none;stroke:#000000;stroke-miterlimit:10;"/>
</g>
</svg>

View File

@ -0,0 +1,102 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { MessageViewer } from '@anticrm/presentation'
import { getPlatformColor, Label } from '@anticrm/ui'
import view from '../../plugin'
export let href: string
interface Assignee {
login: string
html_url: string
}
interface Label {
name: string
}
interface Data {
number: string
body: string | undefined
title: string
assignees: Assignee[]
labels: Label[]
}
async function getData (href: string): Promise<Data> {
const params = href.replace(/(http.:\/\/)?(www.)?github.com\//, '')
const res = await (await fetch(`https://api.github.com/repos/${params}`)).json()
return {
number: res.number,
body: format(res.body),
title: res.title,
assignees: res.assignees,
labels: res.labels
}
}
function format (body: string | undefined): string | undefined {
if (!body) return undefined
return body.replace(/[\r?\n]+/g, '<br />')
.replace(/```(.+?)```/g, '<pre><code>$1</code></pre>')
.replace(/`(.+?)`/g, '<code>$1</code>')
}
</script>
<div class="flex mt-2">
<div class="line" style="background-color: {getPlatformColor(7)}" />
{#await getData(href) then data}
<div class="flex-col">
<a class="fs-title mb-1" {href}>#{data.number} {data.title}</a>
{#if data.body}
<div>
<MessageViewer message={data.body} />
</div>
{/if}
<div class="flex-between">
{#if data.assignees.length}
<div class="flex-col">
<div class="fs-title"><Label label={view.string.Assignees} /></div>
<div>
{#each data.assignees as assignee}
<a href={assignee.html_url}>@{assignee.login}</a>
{/each}
</div>
</div>
{/if}
{#if data.labels.length}
<div class="flex-col">
<div class="fs-title"><Label label={view.string.Labels} /></div>
<div>
{#each data.labels as label, i}
{#if i}, {/if}
{label.name}
{/each}
</div>
</div>
{/if}
</div>
</div>
{/await}
</div>
<style lang="scss">
.line {
margin-right: 1rem;
width: 0.4rem;
border-radius: 0.25rem;
}
</style>

View File

@ -0,0 +1,99 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import Play from '../icons/Play.svelte'
import { getPlatformColor } from '@anticrm/ui'
export let href: string
const maxWidth = 400
const maxHeight = 400
let height = maxHeight
let emb: HTMLDivElement | undefined
interface Data {
author_url: string
author: string
thumbnail: string
title: string
html: string
}
let played = false
async function getData (href: string): Promise<Data> {
const res = await (await fetch(`https://www.youtube.com/oembed?url=${href}&format=json&maxwidth=${maxWidth}&maxheight=${maxHeight}`)).json()
height = (res.thumbnail_height / res.thumbnail_width) * maxWidth
return {
author_url: res.author_url,
author: res.author_name,
thumbnail: res.thumbnail_url,
title: res.title,
html: res.html
}
}
function setHeigh (emb: HTMLElement): void {
const child = (emb.firstElementChild as HTMLElement)
child.style.height = `${height}px`
child.setAttribute('height', `${height}px`)
}
$: emb && setHeigh(emb)
</script>
<div class="flex mt-2">
<div class="line" style="background-color: {getPlatformColor(2)}" />
{#await getData(href) then data}
<div class="flex-col">
<div class="mb-1"><a class="fs-title" {href} >{data.title}</a></div>
<div class="mb-1"><a href={data.author_url} >{data.author}</a></div>
{#if !played}
<div
class="container"
on:click={() => {
played = true
}}
>
<img width="400px" src={data.thumbnail} alt={data.title}/>
<div class="play-btn"><Play size={'full'} /></div>
</div>
{:else}
<div bind:this={emb}>
{@html data.html}
</div>
{/if}
</div>
{/await}
</div>
<style lang="scss">
.line {
margin-right: 1rem;
width: 0.4rem;
border-radius: 0.25rem;
}
.container {
position: relative;
cursor: pointer;
.play-btn {
position: absolute;
top: calc(50% - 50px);
left: calc(50% - 50px);
height: 100px;
width: 100px;
}
}
</style>

View File

@ -35,12 +35,15 @@ import Table from './components/Table.svelte'
import TableView from './components/TableView.svelte'
import TimestampPresenter from './components/TimestampPresenter.svelte'
import UpDownNavigator from './components/UpDownNavigator.svelte'
import GithubPresenter from './components/linkPresenters/GithubPresenter.svelte'
import YoutubePresenter from './components/linkPresenters/YoutubePresenter.svelte'
export { getActions } from './actions'
export { default as ActionContext } from './components/ActionContext.svelte'
export { default as ActionHandler } from './components/ActionHandler.svelte'
export { default as ContextMenu } from './components/Menu.svelte'
export { default as TableBrowser } from './components/TableBrowser.svelte'
export { default as LinkPresenter } from './components/LinkPresenter.svelte'
export * from './context'
export * from './selection'
export { buildModel, getCollectionCounter, getObjectPresenter, LoadingProps } from './utils'
@ -64,6 +67,8 @@ export default async (): Promise<Resources> => ({
ObjectPresenter,
EditDoc,
HTMLPresenter,
IntlStringPresenter
IntlStringPresenter,
GithubPresenter,
YoutubePresenter
}
})

View File

@ -28,6 +28,8 @@ export default mergeIds(viewId, view, {
LabelNA: '' as IntlString,
ChooseAColor: '' as IntlString,
DeleteObject: '' as IntlString,
DeleteObjectConfirm: '' as IntlString
DeleteObjectConfirm: '' as IntlString,
Assignees: '' as IntlString,
Labels: '' as IntlString
}
})

View File

@ -97,6 +97,14 @@ export interface Viewlet extends Doc {
config: any
}
/**
* @public
*/
export interface LinkPresenter extends Doc {
pattern: string
component: AnyComponent
}
/**
* @public
*
@ -264,7 +272,8 @@ const view = plugin(viewId, {
ViewletDescriptor: '' as Ref<Class<ViewletDescriptor>>,
Viewlet: '' as Ref<Class<Viewlet>>,
Action: '' as Ref<Class<Action>>,
ActionTarget: '' as Ref<Class<ActionTarget>>
ActionTarget: '' as Ref<Class<ActionTarget>>,
LinkPresenter: '' as Ref<Class<LinkPresenter>>
},
viewlet: {
Table: '' as Ref<ViewletDescriptor>