Skip to content

Commit

Permalink
SPACE EMOJIS
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexAndBear committed Mar 6, 2024
1 parent d84e6e9 commit a9d0c7f
Show file tree
Hide file tree
Showing 17 changed files with 455 additions and 6 deletions.
1 change: 1 addition & 0 deletions packages/design-system/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"copy-webpack-plugin": "^11.0.0",
"css-loader": "6.8.1",
"deepmerge": "^4.2.2",
"emoji-mart": "^5.5.2",
"file-loader": "^6.2.0",
"filesize": "^10.1.0",
"focus-trap": "7.2.0",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<template>
<div ref="emojiPickerRef"></div>
</template>

<script lang="ts">
import { defineComponent, nextTick, ref, unref, watch } from 'vue'
import { useGettext } from 'vue3-gettext'
import { Picker } from 'emoji-mart'
/**
* Emoji Picker
*/
export default defineComponent({
name: 'OcEmojiPicker',
status: 'ready',
release: '1.0.0',
props: {
theme: {
type: String,
default: 'light',
validator: (value: string) => {
return ['light', 'dark'].includes(value)
}
}
},
emits: ['emojiSelect', 'clickOutside'],
setup(props, { emit }) {
const { $gettext, current: currentLanguage } = useGettext()
const emojiPickerRef = ref<HTMLElement>()
watch(
[() => props.theme, currentLanguage],
async () => {
await nextTick()
const i18n = {
search: $gettext('Search'),
search_no_results_1: $gettext('Oh no!'),
search_no_results_2: $gettext('That emoji couldn’t be found'),
pick: $gettext('Pick an emoji…'),
add_custom: $gettext('Add custom emoji'),
categories: {
activity: $gettext('Activity'),
custom: $gettext('Custom'),
flags: $gettext('Flags'),
foods: $gettext('Food & Drink'),
frequent: $gettext('Frequently used'),
nature: $gettext('Animals & Nature'),
objects: $gettext('Objects'),
people: $gettext('Smileys & People'),
places: $gettext('Travel & Places'),
search: $gettext('Search Results'),
symbols: $gettext('Symbols')
},
skins: {
choose: $gettext('Choose default skin tone'),
'1': $gettext('Default'),
'2': $gettext('Light'),
'3': $gettext('Medium-Light'),
'4': $gettext('Medium'),
'5': $gettext('Medium-Dark'),
'6': $gettext('Dark')
}
}
const pickerOptions = {
onEmojiSelect: (emoji: any) => emit('emojiSelect', emoji.native),
onClickOutside: () => emit('clickOutside'),
i18n,
theme: props.theme
}
const picker = new Picker(pickerOptions)
unref(emojiPickerRef).innerHTML = ''
unref(emojiPickerRef).appendChild(picker as any)
},
{ immediate: true }
)
return {
emojiPickerRef
}
}
})
</script>
1 change: 1 addition & 0 deletions packages/design-system/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ export { default as OcTag } from './OcTag/OcTag.vue'
export { default as OcTextarea } from './OcTextarea/OcTextarea.vue'
export { default as OcTextInput } from './OcTextInput/OcTextInput.vue'
export { default as OcErrorLog } from './OcErrorLog/OcErrorLog.vue'
export { default as OcEmojiPicker } from './OcEmojiPicker/OcEmojiPicker.vue'
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@
<script lang="ts">
import { computed, defineComponent, inject, Ref, ref, unref, VNodeRef } from 'vue'
import { Resource, SpaceResource } from '@ownclouders/web-client'
import { ActionMenuItem, FileActionOptions, SpaceActionOptions } from '@ownclouders/web-pkg'
import {
ActionMenuItem,
FileActionOptions,
SpaceActionOptions,
useSpaceActionsSetIcon
} from '@ownclouders/web-pkg'
import { usePreviewService } from '@ownclouders/web-pkg'
import {
useSpaceActionsDelete,
Expand Down Expand Up @@ -66,6 +71,7 @@ export default defineComponent({
const { actions: uploadImageActions, uploadImageSpace } = useSpaceActionsUploadImage({
spaceImageInput
})
const { actions: setSpaceIconActions } = useSpaceActionsSetIcon()
const { actions: downloadArchiveActions } = useFileActionsDownloadArchive()
const actions = computed(() =>
Expand All @@ -75,6 +81,7 @@ export default defineComponent({
...unref(duplicateActions),
...unref(editDescriptionActions),
...unref(uploadImageActions),
...unref(setSpaceIconActions),
...unref(editReadmeContentActions),
...unref(editQuotaActions),
...unref(restoreActions),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import {
useSpaceActionsEditReadmeContent,
useSpaceActionsRename,
useSpaceActionsRestore,
useSpaceActionsShowMembers
useSpaceActionsShowMembers,
useSpaceActionsSetIcon
} from '@ownclouders/web-pkg'
import { isLocationSpacesActive } from '@ownclouders/web-pkg'
import { computed, defineComponent, PropType, Ref, ref, toRef, unref, VNodeRef } from 'vue'
Expand Down Expand Up @@ -60,6 +61,7 @@ export default defineComponent({
const { actions: editQuotaActions } = useSpaceActionsEditQuota()
const { actions: editReadmeContentActions } = useSpaceActionsEditReadmeContent()
const { actions: editDescriptionActions } = useSpaceActionsEditDescription()
const { actions: setSpaceIconActions } = useSpaceActionsSetIcon()
const { actions: renameActions } = useSpaceActionsRename()
const { actions: restoreActions } = useSpaceActionsRestore()
const { actions: showDetailsActions } = useFileActionsShowDetails()
Expand All @@ -83,7 +85,8 @@ export default defineComponent({
...unref(renameActions),
...unref(duplicateActions),
...unref(editDescriptionActions),
...unref(uploadImageActions)
...unref(uploadImageActions),
...unref(setSpaceIconActions)
]
if (isLocationSpacesActive(router, 'files-spaces-generic')) {
Expand Down
4 changes: 2 additions & 2 deletions packages/web-client/src/webdav/client/dav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export class DAV {

public put(
path: string,
content: string,
content: string | ArrayBuffer,
{
headers = {},
onUploadProgress,
Expand Down Expand Up @@ -168,7 +168,7 @@ export class DAV {
headers,
options
}: {
body?: string
body?: string | ArrayBuffer
headers?: Headers
options?: Partial<RequestOptionsCustom>
} = {}
Expand Down
2 changes: 1 addition & 1 deletion packages/web-client/src/webdav/putFileContents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const PutFileContentsFactory = (
onUploadProgress = null
}: {
path?: string
content?: string
content?: string | ArrayBuffer
previousEntityTag?: string
headers?: Record<string, string>
overwrite?: boolean
Expand Down
40 changes: 40 additions & 0 deletions packages/web-pkg/src/components/Modals/EmojiPickerModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<template>
<oc-emoji-picker :theme="theme" @emoji-select="onEmojiSelect" />
</template>

<script lang="ts">
import { computed, defineComponent, onMounted, PropType, unref } from 'vue'
import { Modal, useThemeStore } from '../../composables'
import { storeToRefs } from 'pinia'
export default defineComponent({
name: 'EmojiPickerModal',
components: {},
props: {
modal: { type: Object as PropType<Modal>, required: true }
},
emits: ['confirm'],
setup(props, { emit }) {
const themeStore = useThemeStore()
const { currentTheme } = storeToRefs(themeStore)
const theme = computed(() => {
return unref(currentTheme).isDark ? 'dark' : 'light'
})
const onEmojiSelect = (emoji: string) => {
emit('confirm', emoji)
}
onMounted(() => {
const modalEl = document.querySelector<HTMLElement>('.oc-modal')
modalEl.style.width = 'auto'
})
return {
onEmojiSelect,
theme
}
}
})
</script>
1 change: 1 addition & 0 deletions packages/web-pkg/src/components/Modals/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { default as ResourceConflictModal } from './ResourceConflictModal.vue'
export { default as SpaceMoveInfoModal } from './SpaceMoveInfoModal.vue'
export { default as EmojiPickerModal } from './EmojiPickerModal.vue'
1 change: 1 addition & 0 deletions packages/web-pkg/src/composables/actions/spaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './useSpaceActionsRename'
export * from './useSpaceActionsRestore'
export * from './useSpaceActionsShowMembers'
export * from './useSpaceActionsNavigateToTrash'
export * from './useSpaceActionsSetIcon'
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { SpaceResource } from '@ownclouders/web-client'
import { computed } from 'vue'
import { SpaceAction, SpaceActionOptions } from '../types'
import { useClientService } from '../../clientService'
import { useLoadingService } from '../../loadingService'
import { useGettext } from 'vue3-gettext'
import { useMessages, useModals, useSpacesStore, useUserStore } from '../../piniaStores'
import { useCreateSpace } from '../../spaces'
import { buildSpace } from '@ownclouders/web-client/src/helpers'
import { eventBus } from '../../../services'
import { Drive } from '@ownclouders/web-client/src/generated'
import { blobToArrayBuffer, canvasToBlob } from '../../../helpers'
import EmojiPickerModal from '../../../components/Modals/EmojiPickerModal.vue'

export const useSpaceActionsSetIcon = () => {
const userStore = useUserStore()
const { showMessage, showErrorMessage } = useMessages()
const { $gettext } = useGettext()
const clientService = useClientService()
const loadingService = useLoadingService()
const { createDefaultMetaFolder } = useCreateSpace()
const spacesStore = useSpacesStore()
const { dispatchModal } = useModals()
const handler = ({ resources }: SpaceActionOptions) => {
if (resources.length !== 1) {
return
}

dispatchModal({
title: $gettext('Set icon for %{space}', { space: resources[0].name }),
hideConfirmButton: true,
customComponent: EmojiPickerModal,
onConfirm: (emoji: string) => setIconSpace(resources[0], emoji)
})
}

const generateEmojiImage = async (emoji: string): Promise<ArrayBuffer | string> => {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
const aspectRatio = 16 / 9,
width = 720,
height = width / aspectRatio

canvas.width = width
canvas.height = height

const textSize = 0.4 * width
context.font = `${textSize}px sans-serif`

context.textBaseline = 'middle'
context.textAlign = 'center'

context.fillText(emoji, canvas.width / 2, canvas.height / 2)

const blob = await canvasToBlob(canvas)
return blobToArrayBuffer(blob)
}

const setIconSpace = async (space: SpaceResource, emoji: string) => {
const graphClient = clientService.graphAuthenticated
const content = await generateEmojiImage(emoji)

try {
await clientService.webdav.getFileInfo(space, { path: '.space' })
} catch (_) {
spacesStore.updateSpaceField({
id: space.id,
field: 'spaceReadmeData',
value: (await createDefaultMetaFolder(space)).spaceReadmeData
})
}

return loadingService.addTask(async () => {
const headers = {
'Content-Type': 'application/offset+octet-stream'
}

try {
const { fileId } = await clientService.webdav.putFileContents(space, {
path: `/.space/emoji.png`,
content,
headers,
overwrite: true
})

const { data } = await graphClient.drives.updateDrive(
space.id.toString(),
{
special: [
{
specialFolder: {
name: 'image'
},
id: fileId
}
]
} as Drive,
{}
)

spacesStore.updateSpaceField({
id: space.id.toString(),
field: 'spaceImageData',
value: data.special.find((special) => special.specialFolder.name === 'image')
})
showMessage({ title: $gettext('Space icon was set successfully') })
eventBus.publish('app.files.spaces.uploaded-image', buildSpace(data))
} catch (error) {
console.error(error)
showErrorMessage({
title: $gettext('Failed to set space icon'),
errors: [error]
})
}
})
}

const actions = computed((): SpaceAction[] => [
{
name: 'set-space-icon',
icon: 'emoji-sticker',
handler,
label: () => {
return $gettext('Set icon')
},
isVisible: ({ resources }) => {
if (resources.length !== 1) {
return false
}

return resources[0].canEditImage({ user: userStore.user })
},
componentType: 'button',
class: 'oc-files-actions-set-space-icon-trigger'
}
])

return {
actions,
setIconSpace
}
}
12 changes: 12 additions & 0 deletions packages/web-pkg/src/helpers/binary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const blobToArrayBuffer: (blob: Blob) => Promise<string | ArrayBuffer> = (blob: Blob) => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onloadend = () => resolve(reader.result)
reader.onerror = (e) => reject(e)
reader.readAsArrayBuffer(blob)
})
}

export const canvasToBlob = (canvas: HTMLCanvasElement): Promise<Blob> => {
return new Promise((resolve) => canvas.toBlob(resolve))
}
1 change: 1 addition & 0 deletions packages/web-pkg/src/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export * from './locale'
export * from './path'
export * from './statusIndicators'
export * from './store'
export * from './binary'
Loading

0 comments on commit a9d0c7f

Please sign in to comment.