Skip to content

Commit

Permalink
feat: Show announcements
Browse files Browse the repository at this point in the history
  • Loading branch information
cballevre committed Sep 3, 2024
1 parent 95bb9e4 commit 8774b57
Show file tree
Hide file tree
Showing 14 changed files with 549 additions and 4 deletions.
10 changes: 10 additions & 0 deletions manifest.webapp
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@
"identities": {
"description": "Required to display identities debug data",
"type": "io.cozy.identities"
},
"announcements-dev": {
"type": "cc.cozycloud.announcements.dev",
"verbs": ["GET"],
"description": "Remote-doctype required to get announcements, for development purposes"
},
"announcements-uploads-dev": {
"type": "cc.cozycloud.announcements.dev.uploads",
"verbs": ["GET"],
"description": "Remote-doctype required to get announcements images, for development purposes"
}
},
"routes": {
Expand Down
44 changes: 44 additions & 0 deletions src/components/Announcements/Announcements.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React, { FC, useState } from 'react'
import { differenceInHours } from 'date-fns'

import flag from 'cozy-flags'

import { AnnouncementsDialog } from './AnnouncementsDialog'
import { useAnnouncements } from 'hooks/useAnnouncements'
import { AnnouncementsConfigFlag } from './types'
import { useAnnouncementsSettings } from 'hooks/useAnnouncementsSettings'

const Announcements: FC = () => {
const config = flag<AnnouncementsConfigFlag>('home.announcements')
const [hasBeenDismissed, setBeenDismissed] = useState(false)
const { values, save } = useAnnouncementsSettings()

const handleDismiss = (): void => {
save({
dismissedAt: new Date().toISOString()
})
setBeenDismissed(true)
}

const moreThan = config?.delayAfterDismiss ?? 24
const hasBeenDismissedForMoreThan = values.dismissedAt
? differenceInHours(Date.parse(values.dismissedAt), new Date()) >= moreThan
: true
const canBeDisplayed = !hasBeenDismissed && hasBeenDismissedForMoreThan
const announcements = useAnnouncements({
canBeDisplayed
})

if (canBeDisplayed && announcements.length > 0) {
return (
<AnnouncementsDialog
announcements={announcements}
onDismiss={handleDismiss}
/>
)
}

return null
}

export { Announcements }
101 changes: 101 additions & 0 deletions src/components/Announcements/AnnouncementsDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React, { useState, FC } from 'react'
import SwipeableViews from 'react-swipeable-views'

import { FixedActionsDialog } from 'cozy-ui/transpiled/react/CozyDialogs'
import CozyTheme from 'cozy-ui/transpiled/react/providers/CozyTheme'
import MobileStepper from 'cozy-ui/transpiled/react/MobileStepper'
import IconButton from 'cozy-ui/transpiled/react/IconButton'
import Icon from 'cozy-ui/transpiled/react/Icon'
import LeftIcon from 'cozy-ui/transpiled/react/Icons/Left'
import RightIcon from 'cozy-ui/transpiled/react/Icons/Right'
import useBreakpoints from 'cozy-ui/transpiled/react/providers/Breakpoints'

import { AnnouncementsDialogContent } from './AnnouncementsDialogContent'
import { Announcement } from './types'
import { useAnnouncementsSettings } from 'hooks/useAnnouncementsSettings'

interface AnnouncementsDialogProps {
announcements: Array<Announcement>
onDismiss: () => void
}

const AnnouncementsDialog: FC<AnnouncementsDialogProps> = ({
announcements,
onDismiss
}) => {
const { values, save } = useAnnouncementsSettings()
const { isMobile } = useBreakpoints()

const [activeStep, setActiveStep] = useState(0)

const handleBack = (): void => {
setActiveStep(activeStep - 1)
}

const handleNext = (): void => {
const uuid = announcements[activeStep].attributes.uuid
if (!values?.seen.includes(uuid)) {
save({
seen: [...(values?.seen ?? []), uuid]
})
}
setActiveStep(activeStep + 1)
}

const handleChangedIndex = (index: number): void => {
setActiveStep(index)
}

const maxSteps = announcements.length

return (
<CozyTheme variant="normal">
<FixedActionsDialog
open
onClose={onDismiss}
content={
<SwipeableViews
index={activeStep}
onChangeIndex={handleChangedIndex}
animateTransitions={isMobile}
>
{announcements.map((announcement, index) => (
<AnnouncementsDialogContent
key={index}
isLast={index === maxSteps - 1}
announcement={announcement}
onDismiss={onDismiss}
onNext={handleNext}
/>
))}
</SwipeableViews>
}
actions={
maxSteps > 1 ? (
<MobileStepper
className="u-mh-auto"
steps={maxSteps}
position="static"
activeStep={activeStep}
nextButton={
<IconButton
onClick={handleNext}
disabled={activeStep === maxSteps - 1}
>
<Icon icon={RightIcon} />
</IconButton>
}
backButton={
<IconButton onClick={handleBack} disabled={activeStep === 0}>
<Icon icon={LeftIcon} />
</IconButton>
}
/>
) : null
}
/>
</CozyTheme>
)
}

export { AnnouncementsDialog }
105 changes: 105 additions & 0 deletions src/components/Announcements/AnnouncementsDialogContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import React, { FC } from 'react'
import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n'
import Typography from 'cozy-ui/transpiled/react/Typography'
import Buttons from 'cozy-ui/transpiled/react/Buttons'

import { Announcement } from './types'
import { useAnnouncementsImage } from 'hooks/useAnnouncementsImage'

interface AnnouncementsDialogContentProps {
isLast: boolean
announcement: Announcement
onDismiss: () => void
onNext: () => void
}

const AnnouncementsDialogContent: FC<AnnouncementsDialogContentProps> = ({
isLast,
announcement,
onDismiss,
onNext
}) => {
const { t, f } = useI18n()
const primaryImage = useAnnouncementsImage(
announcement.attributes.primary_image.data.attributes.formats.small.url
)
const secondaryImage = useAnnouncementsImage(
announcement.attributes.secondary_image.data?.attributes.formats.thumbnail
.url
)

const handleMainAction = (): void => {
if (announcement.attributes.main_action?.link) {
window.open(announcement.attributes.main_action.link, '_blank')
}
}

return (
<div className="u-flex u-flex-column u-flex-items-center">
{primaryImage ? (
<img
src={primaryImage}
alt={
announcement.attributes.primary_image.data.attributes
.alternativeText
}
className="u-mt-1 u-mb-2 u-bdrs-3 u-maw-100"
style={{
objectFit: 'cover',
objectPosition: '100% 0'
}}
/>
) : null}
<Typography align="center" className="u-mb-half" variant="h3">
{announcement.attributes.title}
</Typography>
<Typography
align="center"
color="textSecondary"
className="u-mb-1"
variant="body2"
>
{f(
announcement.attributes.start_at,
t('AnnouncementsDialogContent.dateFormat')
)}
</Typography>
<Typography align="center" className="u-mb-1">
{announcement.attributes.content}
</Typography>
{announcement.attributes.main_action ? (
<Buttons
className="u-mb-half"
variant="secondary"
label={announcement.attributes.main_action.label}
onClick={handleMainAction}
/>
) : null}
<Buttons
label={t(
isLast
? 'AnnouncementsDialogContent.understand'
: 'AnnouncementsDialogContent.next'
)}
variant="secondary"
onClick={isLast ? onDismiss : onNext}
/>
{secondaryImage ? (
<img
src={secondaryImage}
alt={
announcement.attributes.secondary_image.data?.attributes
.alternativeText
}
className="u-mt-1 u-w-2 u-h-2"
style={{
objectFit: 'cover',
objectPosition: '100% 0'
}}
/>
) : null}
</div>
)
}

export { AnnouncementsDialogContent }
19 changes: 19 additions & 0 deletions src/components/Announcements/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Announcement } from './types'

export const getUnseenAnnouncements = (
data: Announcement[],
announcements_seen: string[]
): Announcement[] => {
return data.filter(announcement => {
if (announcements_seen) {
return !announcements_seen.includes(announcement.attributes.uuid)
}
return true
})
}

export const isAnnouncement = (
announcement: unknown
): announcement is Announcement => {
return (announcement as Announcement).attributes?.title !== undefined
}
46 changes: 46 additions & 0 deletions src/components/Announcements/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
export interface Announcement {
id: string
type: string
attributes: {
title: string
content: string
start_at: string
uuid: string
main_action?: {
label: string
link: string
}
primary_image: {
data: {
attributes: {
formats: {
small: {
url: string
}
}
alternativeText?: string
}
}
}
secondary_image: {
data: {
attributes: {
formats: {
thumbnail: {
url: string
}
}
alternativeText?: string
}
} | null
}
}
}

export interface AnnouncementsConfig {
remoteDoctype: string
channels: string
delayAfterDismiss: number
}

export type AnnouncementsConfigFlag = AnnouncementsConfig | null
2 changes: 2 additions & 0 deletions src/containers/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n'
import SectionDialog from 'components/Sections/SectionDialog'
import { SentryRoutes } from 'lib/sentry'
import { Announcements } from 'components/Announcements/Announcements'

window.flag = window.flag || flag
window.minilog = minilog
Expand Down Expand Up @@ -134,6 +135,7 @@ const App = ({ accounts, konnectors, triggers }) => {
<ReloadFocus />
<MainView>
<BackupNotification />
<Announcements />
<Corner />
<div
className="u-flex u-flex-column u-flex-content-start u-flex-content-stretch u-w-100 u-m-auto u-pos-relative"
Expand Down
23 changes: 20 additions & 3 deletions src/cozy-ui.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,15 @@ declare module 'cozy-ui/transpiled/react/CozyDialogs' {

const Dialog: (props: DialogProps) => JSX.Element
const ConfirmDialog: (props: ConfirmDialogProps) => JSX.Element

export { ConfirmDialog, ConfirmDialogProps, Dialog, DialogProps }
const FixedActionsDialog: (props: DialogProps) => JSX.Element

export {
ConfirmDialog,
FixedActionsDialog,
ConfirmDialogProps,
Dialog,
DialogProps
}
}

declare module 'cozy-ui/transpiled/react/providers/CozyTheme' {
Expand All @@ -65,7 +72,11 @@ declare module 'cozy-ui/transpiled/react/providers/CozyTheme' {
}

declare module 'cozy-ui/transpiled/react/providers/I18n' {
export const useI18n: () => { t: (key: string) => string; lang: string }
export const useI18n: () => {
t: (key: string) => string
lang: string
f: (date: string, format: string) => string
}
}

declare module 'cozy-ui/transpiled/react/Buttons' {
Expand Down Expand Up @@ -104,3 +115,9 @@ declare module 'cozy-ui/transpiled/react/styles' {
declare module 'cozy-ui/react/Avatar/helpers' {
export function nameToColor(name: string): string
}

declare module 'cozy-ui/transpiled/react/Typography' {
export default function Typography(
props: Record<string, unknown>
): JSX.Element
}
2 changes: 2 additions & 0 deletions src/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ declare module 'assets/*' {
const assets: string
export default assets
}

declare module 'react-swipeable-views'
Loading

0 comments on commit 8774b57

Please sign in to comment.