Skip to content

Commit 8774b57

Browse files
committed
feat: Show announcements
1 parent 95bb9e4 commit 8774b57

File tree

14 files changed

+549
-4
lines changed

14 files changed

+549
-4
lines changed

manifest.webapp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,16 @@
9898
"identities": {
9999
"description": "Required to display identities debug data",
100100
"type": "io.cozy.identities"
101+
},
102+
"announcements-dev": {
103+
"type": "cc.cozycloud.announcements.dev",
104+
"verbs": ["GET"],
105+
"description": "Remote-doctype required to get announcements, for development purposes"
106+
},
107+
"announcements-uploads-dev": {
108+
"type": "cc.cozycloud.announcements.dev.uploads",
109+
"verbs": ["GET"],
110+
"description": "Remote-doctype required to get announcements images, for development purposes"
101111
}
102112
},
103113
"routes": {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React, { FC, useState } from 'react'
2+
import { differenceInHours } from 'date-fns'
3+
4+
import flag from 'cozy-flags'
5+
6+
import { AnnouncementsDialog } from './AnnouncementsDialog'
7+
import { useAnnouncements } from 'hooks/useAnnouncements'
8+
import { AnnouncementsConfigFlag } from './types'
9+
import { useAnnouncementsSettings } from 'hooks/useAnnouncementsSettings'
10+
11+
const Announcements: FC = () => {
12+
const config = flag<AnnouncementsConfigFlag>('home.announcements')
13+
const [hasBeenDismissed, setBeenDismissed] = useState(false)
14+
const { values, save } = useAnnouncementsSettings()
15+
16+
const handleDismiss = (): void => {
17+
save({
18+
dismissedAt: new Date().toISOString()
19+
})
20+
setBeenDismissed(true)
21+
}
22+
23+
const moreThan = config?.delayAfterDismiss ?? 24
24+
const hasBeenDismissedForMoreThan = values.dismissedAt
25+
? differenceInHours(Date.parse(values.dismissedAt), new Date()) >= moreThan
26+
: true
27+
const canBeDisplayed = !hasBeenDismissed && hasBeenDismissedForMoreThan
28+
const announcements = useAnnouncements({
29+
canBeDisplayed
30+
})
31+
32+
if (canBeDisplayed && announcements.length > 0) {
33+
return (
34+
<AnnouncementsDialog
35+
announcements={announcements}
36+
onDismiss={handleDismiss}
37+
/>
38+
)
39+
}
40+
41+
return null
42+
}
43+
44+
export { Announcements }
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import React, { useState, FC } from 'react'
2+
import SwipeableViews from 'react-swipeable-views'
3+
4+
import { FixedActionsDialog } from 'cozy-ui/transpiled/react/CozyDialogs'
5+
import CozyTheme from 'cozy-ui/transpiled/react/providers/CozyTheme'
6+
import MobileStepper from 'cozy-ui/transpiled/react/MobileStepper'
7+
import IconButton from 'cozy-ui/transpiled/react/IconButton'
8+
import Icon from 'cozy-ui/transpiled/react/Icon'
9+
import LeftIcon from 'cozy-ui/transpiled/react/Icons/Left'
10+
import RightIcon from 'cozy-ui/transpiled/react/Icons/Right'
11+
import useBreakpoints from 'cozy-ui/transpiled/react/providers/Breakpoints'
12+
13+
import { AnnouncementsDialogContent } from './AnnouncementsDialogContent'
14+
import { Announcement } from './types'
15+
import { useAnnouncementsSettings } from 'hooks/useAnnouncementsSettings'
16+
17+
interface AnnouncementsDialogProps {
18+
announcements: Array<Announcement>
19+
onDismiss: () => void
20+
}
21+
22+
const AnnouncementsDialog: FC<AnnouncementsDialogProps> = ({
23+
announcements,
24+
onDismiss
25+
}) => {
26+
const { values, save } = useAnnouncementsSettings()
27+
const { isMobile } = useBreakpoints()
28+
29+
const [activeStep, setActiveStep] = useState(0)
30+
31+
const handleBack = (): void => {
32+
setActiveStep(activeStep - 1)
33+
}
34+
35+
const handleNext = (): void => {
36+
const uuid = announcements[activeStep].attributes.uuid
37+
if (!values?.seen.includes(uuid)) {
38+
save({
39+
seen: [...(values?.seen ?? []), uuid]
40+
})
41+
}
42+
setActiveStep(activeStep + 1)
43+
}
44+
45+
const handleChangedIndex = (index: number): void => {
46+
setActiveStep(index)
47+
}
48+
49+
const maxSteps = announcements.length
50+
51+
return (
52+
<CozyTheme variant="normal">
53+
<FixedActionsDialog
54+
open
55+
onClose={onDismiss}
56+
content={
57+
<SwipeableViews
58+
index={activeStep}
59+
onChangeIndex={handleChangedIndex}
60+
animateTransitions={isMobile}
61+
>
62+
{announcements.map((announcement, index) => (
63+
<AnnouncementsDialogContent
64+
key={index}
65+
isLast={index === maxSteps - 1}
66+
announcement={announcement}
67+
onDismiss={onDismiss}
68+
onNext={handleNext}
69+
/>
70+
))}
71+
</SwipeableViews>
72+
}
73+
actions={
74+
maxSteps > 1 ? (
75+
<MobileStepper
76+
className="u-mh-auto"
77+
steps={maxSteps}
78+
position="static"
79+
activeStep={activeStep}
80+
nextButton={
81+
<IconButton
82+
onClick={handleNext}
83+
disabled={activeStep === maxSteps - 1}
84+
>
85+
<Icon icon={RightIcon} />
86+
</IconButton>
87+
}
88+
backButton={
89+
<IconButton onClick={handleBack} disabled={activeStep === 0}>
90+
<Icon icon={LeftIcon} />
91+
</IconButton>
92+
}
93+
/>
94+
) : null
95+
}
96+
/>
97+
</CozyTheme>
98+
)
99+
}
100+
101+
export { AnnouncementsDialog }
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import React, { FC } from 'react'
2+
import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n'
3+
import Typography from 'cozy-ui/transpiled/react/Typography'
4+
import Buttons from 'cozy-ui/transpiled/react/Buttons'
5+
6+
import { Announcement } from './types'
7+
import { useAnnouncementsImage } from 'hooks/useAnnouncementsImage'
8+
9+
interface AnnouncementsDialogContentProps {
10+
isLast: boolean
11+
announcement: Announcement
12+
onDismiss: () => void
13+
onNext: () => void
14+
}
15+
16+
const AnnouncementsDialogContent: FC<AnnouncementsDialogContentProps> = ({
17+
isLast,
18+
announcement,
19+
onDismiss,
20+
onNext
21+
}) => {
22+
const { t, f } = useI18n()
23+
const primaryImage = useAnnouncementsImage(
24+
announcement.attributes.primary_image.data.attributes.formats.small.url
25+
)
26+
const secondaryImage = useAnnouncementsImage(
27+
announcement.attributes.secondary_image.data?.attributes.formats.thumbnail
28+
.url
29+
)
30+
31+
const handleMainAction = (): void => {
32+
if (announcement.attributes.main_action?.link) {
33+
window.open(announcement.attributes.main_action.link, '_blank')
34+
}
35+
}
36+
37+
return (
38+
<div className="u-flex u-flex-column u-flex-items-center">
39+
{primaryImage ? (
40+
<img
41+
src={primaryImage}
42+
alt={
43+
announcement.attributes.primary_image.data.attributes
44+
.alternativeText
45+
}
46+
className="u-mt-1 u-mb-2 u-bdrs-3 u-maw-100"
47+
style={{
48+
objectFit: 'cover',
49+
objectPosition: '100% 0'
50+
}}
51+
/>
52+
) : null}
53+
<Typography align="center" className="u-mb-half" variant="h3">
54+
{announcement.attributes.title}
55+
</Typography>
56+
<Typography
57+
align="center"
58+
color="textSecondary"
59+
className="u-mb-1"
60+
variant="body2"
61+
>
62+
{f(
63+
announcement.attributes.start_at,
64+
t('AnnouncementsDialogContent.dateFormat')
65+
)}
66+
</Typography>
67+
<Typography align="center" className="u-mb-1">
68+
{announcement.attributes.content}
69+
</Typography>
70+
{announcement.attributes.main_action ? (
71+
<Buttons
72+
className="u-mb-half"
73+
variant="secondary"
74+
label={announcement.attributes.main_action.label}
75+
onClick={handleMainAction}
76+
/>
77+
) : null}
78+
<Buttons
79+
label={t(
80+
isLast
81+
? 'AnnouncementsDialogContent.understand'
82+
: 'AnnouncementsDialogContent.next'
83+
)}
84+
variant="secondary"
85+
onClick={isLast ? onDismiss : onNext}
86+
/>
87+
{secondaryImage ? (
88+
<img
89+
src={secondaryImage}
90+
alt={
91+
announcement.attributes.secondary_image.data?.attributes
92+
.alternativeText
93+
}
94+
className="u-mt-1 u-w-2 u-h-2"
95+
style={{
96+
objectFit: 'cover',
97+
objectPosition: '100% 0'
98+
}}
99+
/>
100+
) : null}
101+
</div>
102+
)
103+
}
104+
105+
export { AnnouncementsDialogContent }
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Announcement } from './types'
2+
3+
export const getUnseenAnnouncements = (
4+
data: Announcement[],
5+
announcements_seen: string[]
6+
): Announcement[] => {
7+
return data.filter(announcement => {
8+
if (announcements_seen) {
9+
return !announcements_seen.includes(announcement.attributes.uuid)
10+
}
11+
return true
12+
})
13+
}
14+
15+
export const isAnnouncement = (
16+
announcement: unknown
17+
): announcement is Announcement => {
18+
return (announcement as Announcement).attributes?.title !== undefined
19+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
export interface Announcement {
2+
id: string
3+
type: string
4+
attributes: {
5+
title: string
6+
content: string
7+
start_at: string
8+
uuid: string
9+
main_action?: {
10+
label: string
11+
link: string
12+
}
13+
primary_image: {
14+
data: {
15+
attributes: {
16+
formats: {
17+
small: {
18+
url: string
19+
}
20+
}
21+
alternativeText?: string
22+
}
23+
}
24+
}
25+
secondary_image: {
26+
data: {
27+
attributes: {
28+
formats: {
29+
thumbnail: {
30+
url: string
31+
}
32+
}
33+
alternativeText?: string
34+
}
35+
} | null
36+
}
37+
}
38+
}
39+
40+
export interface AnnouncementsConfig {
41+
remoteDoctype: string
42+
channels: string
43+
delayAfterDismiss: number
44+
}
45+
46+
export type AnnouncementsConfigFlag = AnnouncementsConfig | null

src/containers/App.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n'
4343
import SectionDialog from 'components/Sections/SectionDialog'
4444
import { SentryRoutes } from 'lib/sentry'
45+
import { Announcements } from 'components/Announcements/Announcements'
4546

4647
window.flag = window.flag || flag
4748
window.minilog = minilog
@@ -134,6 +135,7 @@ const App = ({ accounts, konnectors, triggers }) => {
134135
<ReloadFocus />
135136
<MainView>
136137
<BackupNotification />
138+
<Announcements />
137139
<Corner />
138140
<div
139141
className="u-flex u-flex-column u-flex-content-start u-flex-content-stretch u-w-100 u-m-auto u-pos-relative"

src/cozy-ui.d.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,15 @@ declare module 'cozy-ui/transpiled/react/CozyDialogs' {
4242

4343
const Dialog: (props: DialogProps) => JSX.Element
4444
const ConfirmDialog: (props: ConfirmDialogProps) => JSX.Element
45-
46-
export { ConfirmDialog, ConfirmDialogProps, Dialog, DialogProps }
45+
const FixedActionsDialog: (props: DialogProps) => JSX.Element
46+
47+
export {
48+
ConfirmDialog,
49+
FixedActionsDialog,
50+
ConfirmDialogProps,
51+
Dialog,
52+
DialogProps
53+
}
4754
}
4855

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

6774
declare module 'cozy-ui/transpiled/react/providers/I18n' {
68-
export const useI18n: () => { t: (key: string) => string; lang: string }
75+
export const useI18n: () => {
76+
t: (key: string) => string
77+
lang: string
78+
f: (date: string, format: string) => string
79+
}
6980
}
7081

7182
declare module 'cozy-ui/transpiled/react/Buttons' {
@@ -104,3 +115,9 @@ declare module 'cozy-ui/transpiled/react/styles' {
104115
declare module 'cozy-ui/react/Avatar/helpers' {
105116
export function nameToColor(name: string): string
106117
}
118+
119+
declare module 'cozy-ui/transpiled/react/Typography' {
120+
export default function Typography(
121+
props: Record<string, unknown>
122+
): JSX.Element
123+
}

src/global.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ declare module 'assets/*' {
1111
const assets: string
1212
export default assets
1313
}
14+
15+
declare module 'react-swipeable-views'

0 commit comments

Comments
 (0)