Add "become a sustainer" button and view to badge dialog
This commit is contained in:
parent
a466b939bc
commit
515943c46c
11 changed files with 197 additions and 32 deletions
|
@ -5296,6 +5296,30 @@
|
|||
"message": "next",
|
||||
"description": "Generic next label"
|
||||
},
|
||||
"BadgeDialog__become-a-sustainer-button": {
|
||||
"message": "Become a Sustainer",
|
||||
"description": "In the badge dialog. This button is shown under sustainer badges, taking users to some instructions"
|
||||
},
|
||||
"BadgeSustainerInstructions__header": {
|
||||
"message": "Become a Sustainer",
|
||||
"description": "In the instructions for becoming a sustainer. The heading."
|
||||
},
|
||||
"BadgeSustainerInstructions__subheader": {
|
||||
"message": "Signal is powered by people like you. Contribute and receive a badge.",
|
||||
"description": "In the instructions for becoming a sustainer. The subheading."
|
||||
},
|
||||
"BadgeSustainerInstructions__instructions__1": {
|
||||
"message": "Open Signal on your phone",
|
||||
"description": "In the instructions for becoming a sustainer. First instruction."
|
||||
},
|
||||
"BadgeSustainerInstructions__instructions__2": {
|
||||
"message": "Tap on your profile photo in the top left to open Settings",
|
||||
"description": "In the instructions for becoming a sustainer. Second instruction."
|
||||
},
|
||||
"BadgeSustainerInstructions__instructions__3": {
|
||||
"message": "Tap on \"Become a Sustainer\" and subscribe",
|
||||
"description": "In the instructions for becoming a sustainer. Third instruction."
|
||||
},
|
||||
"CompositionArea--expand": {
|
||||
"message": "Expand",
|
||||
"description": "Aria label for expanding composition area"
|
||||
|
|
1
images/mobile-settings-dark.svg
Normal file
1
images/mobile-settings-dark.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg height="320" viewBox="0 0 292 320" width="292" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientTransform="matrix(1 0 0 -1 0 -4326)" gradientUnits="userSpaceOnUse" x1="146" x2="146" y1="-4571.26" y2="-4645.78"><stop offset="0" stop-opacity="0"/><stop offset="1" stop-color="#2e2e2e"/></linearGradient><path d="m292 319.78v-267.29a52 52 0 0 0 -51.87-52.13l-187.44-.58a52 52 0 0 0 -52.19 51.94l-.5 268.06z" fill="#5e5e5e" fill-rule="evenodd"/><path d="m279.37 319.78.68-273.17a34.7 34.7 0 0 0 -34.61-34.78l-26.36-.06-143.51-.35-27.17-.07a34.67 34.67 0 0 0 -34.77 34.65l-.63 273.78z" fill="#121212" fill-rule="evenodd"/><path d="m34.51 115.67a22.33 22.33 0 1 1 22.33 22.33 22.32 22.32 0 0 1 -22.33-22.33zm21.74-40.39a13 13 0 1 0 -13-13 13 13 0 0 0 13 13zm151.3 33.65a2.93 2.93 0 0 0 -3-3h-105.07a3 3 0 1 0 0 5.92h105.17a2.88 2.88 0 0 0 2.9-2.92zm-43.88 13.78a2.93 2.93 0 0 0 -3-3h-61.19a3 3 0 1 0 0 5.92h61.29a2.88 2.88 0 0 0 2.9-2.92zm-106.83 77.54a22.3 22.3 0 1 0 -22.33-22.25 22.3 22.3 0 0 0 22.33 22.25zm150.71-29a2.93 2.93 0 0 0 -3-3h-105.07a3 3 0 1 0 0 5.92h105.17a2.88 2.88 0 0 0 2.9-2.96zm-43.88 13.84a2.93 2.93 0 0 0 -3-3h-61.19a3 3 0 1 0 0 5.91h61.29a2.88 2.88 0 0 0 2.9-2.95zm-106.83 77.51a22.3 22.3 0 1 0 -22.33-22.3 22.31 22.31 0 0 0 22.33 22.3zm150.71-29.05a2.93 2.93 0 0 0 -3-3h-105.07a3 3 0 1 0 0 5.91h105.17a2.88 2.88 0 0 0 2.9-2.91zm-43.88 13.79a2.93 2.93 0 0 0 -3-3h-61.19a3 3 0 1 0 0 5.91h61.29a2.88 2.88 0 0 0 2.9-2.91zm43.88 48.5a2.93 2.93 0 0 0 -3-3h-105.07a3 3 0 1 0 0 5.92h105.17a2.88 2.88 0 0 0 2.9-2.92zm-43.88 13.78a2.93 2.93 0 0 0 -3-3h-61.19a3 3 0 1 0 0 5.92h61.29a2.88 2.88 0 0 0 2.9-2.92zm-121 10.16h28.38a22.33 22.33 0 1 0 -28.42 0z" fill="#5e5e5e"/><path d="m0 245.26h292v74.52h-292z" fill="url(#a)"/><path d="m56.25 94.87a33.37 33.37 0 1 1 33.36-33.37 33.41 33.41 0 0 1 -33.36 33.37zm0-60.73a27.37 27.37 0 1 0 27.36 27.36 27.4 27.4 0 0 0 -27.36-27.36z" fill="#6191f3"/></svg>
|
After Width: | Height: | Size: 1.9 KiB |
1
images/mobile-settings-light.svg
Normal file
1
images/mobile-settings-light.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg height="320" viewBox="0 0 292 320" width="292" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><clipPath id="a"><path d="m.34.31h291.66v318.95h-291.66z"/></clipPath><linearGradient id="b" gradientTransform="matrix(1 0 0 -1 0 -4326)" gradientUnits="userSpaceOnUse" x1="145.79" x2="145.79" y1="-4571.48" y2="-4645.26"><stop offset="0" stop-color="#fff" stop-opacity="0"/><stop offset="1" stop-color="#fff"/></linearGradient><linearGradient id="c" x1="146" x2="146" xlink:href="#b" y1="-4571.48" y2="-4646"/><g clip-path="url(#a)"><path d="m.5 51.93a52 52 0 0 1 52.19-51.93l187.44.58a52 52 0 0 1 51.87 52.12l-.93 497.55a52 52 0 0 1 -52.19 51.93l-187.44-.57a52 52 0 0 1 -51.87-52.13z" fill="#dedede" fill-rule="evenodd"/><path d="m48.4 11.57a34.67 34.67 0 0 0 -34.77 34.6l-1.25 507.29a34.69 34.69 0 0 0 34.62 34.77l197 .48a34.69 34.69 0 0 0 34.78-34.6l1.25-507.28a34.7 34.7 0 0 0 -34.61-34.78z" fill="#fff" fill-rule="evenodd"/><g fill="#dbdbdb"><path d="m204.65 111.36h-105.17a3 3 0 1 1 0-5.91h105.11a2.93 2.93 0 0 1 3 3 2.88 2.88 0 0 1 -2.94 2.91z" fill-rule="evenodd"/><path d="m160.77 125.14h-61.29a3 3 0 1 1 0-5.91h61.23a2.93 2.93 0 0 1 3 3 2.88 2.88 0 0 1 -2.94 2.91z" fill-rule="evenodd"/><path d="m204.65 173.65h-105.17a3 3 0 1 1 0-5.92h105.11a2.93 2.93 0 0 1 3 3 2.88 2.88 0 0 1 -2.94 2.92z" fill-rule="evenodd"/><path d="m160.77 187.49h-61.29a3 3 0 1 1 0-5.92h61.23a2.93 2.93 0 0 1 3 3 2.88 2.88 0 0 1 -2.94 2.92z" fill-rule="evenodd"/><path d="m204.65 236h-105.17a3 3 0 1 1 0-5.92h105.11a2.93 2.93 0 0 1 3 3 2.88 2.88 0 0 1 -2.94 2.92z" fill-rule="evenodd"/><path d="m160.77 249.77h-61.29a3 3 0 1 1 0-5.91h61.23a2.93 2.93 0 0 1 3 2.95 2.88 2.88 0 0 1 -2.94 2.96z" fill-rule="evenodd"/><path d="m204.65 298.27h-105.17a3 3 0 1 1 0-5.91h105.11a2.93 2.93 0 0 1 3 2.95 2.88 2.88 0 0 1 -2.94 2.96z" fill-rule="evenodd"/><path d="m34.51 115.15a22.33 22.33 0 1 1 22.33 22.3 22.31 22.31 0 0 1 -22.33-22.3z"/><path d="m56.84 199.73a22.3 22.3 0 1 0 -22.33-22.3 22.31 22.31 0 0 0 22.33 22.3z"/><path d="m56.84 262.07a22.3 22.3 0 1 0 -22.33-22.3 22.31 22.31 0 0 0 22.33 22.3z"/><path d="m56.84 324.42a22.3 22.3 0 1 0 -22.33-22.3 22.31 22.31 0 0 0 22.33 22.3z"/><path d="m163.67 309.1a2.93 2.93 0 0 0 -3-3h-61.19a3 3 0 1 0 0 5.91h61.29a2.88 2.88 0 0 0 2.9-2.91z"/></g><path d="m.24 245.48h291.09v73.78h-291.09z" fill="url(#b)"/><path d="m56.25 74.76a13 13 0 1 0 -13-13 13 13 0 0 0 13 13z" fill="#dbdbdb"/></g><path d="m292 320v-267.3a52 52 0 0 0 -51.87-52.12l-187.44-.58a52 52 0 0 0 -52.19 51.93l-.5 268.07z" fill="#dedede" fill-rule="evenodd"/><path d="m279.37 320 .68-273.17a34.7 34.7 0 0 0 -34.61-34.78l-26.36-.05-143.51-.37-27.17-.06a34.67 34.67 0 0 0 -34.77 34.6l-.63 273.83z" fill="#fff" fill-rule="evenodd"/><path d="m34.51 115.89a22.33 22.33 0 1 1 22.33 22.3 22.32 22.32 0 0 1 -22.33-22.3zm21.74-40.39a13 13 0 1 0 -13-13 13 13 0 0 0 13 13zm151.3 33.65a2.93 2.93 0 0 0 -3-3h-105.07a3 3 0 1 0 0 5.92h105.17a2.88 2.88 0 0 0 2.9-2.92zm-43.88 13.78a2.93 2.93 0 0 0 -3-3h-61.19a3 3 0 1 0 0 5.92h61.29a2.88 2.88 0 0 0 2.9-2.92zm-106.83 77.54a22.3 22.3 0 1 0 -22.33-22.29 22.3 22.3 0 0 0 22.33 22.29zm150.71-29a2.93 2.93 0 0 0 -3-3h-105.07a3 3 0 1 0 0 5.92h105.17a2.88 2.88 0 0 0 2.9-2.96zm-43.88 13.84a2.93 2.93 0 0 0 -3-3h-61.19a3 3 0 1 0 0 5.92h61.29a2.88 2.88 0 0 0 2.9-2.96zm-106.83 77.51a22.3 22.3 0 1 0 -22.33-22.3 22.31 22.31 0 0 0 22.33 22.3zm150.71-29a2.93 2.93 0 0 0 -3-3h-105.07a3 3 0 1 0 0 5.91h105.17a2.88 2.88 0 0 0 2.9-2.96zm-43.88 13.79a2.93 2.93 0 0 0 -3-3h-61.19a3 3 0 1 0 0 5.91h61.29a2.88 2.88 0 0 0 2.9-2.96zm43.88 48.5a2.93 2.93 0 0 0 -3-3h-105.07a3 3 0 1 0 0 5.92h105.17a2.88 2.88 0 0 0 2.9-2.97zm-43.88 13.78a2.93 2.93 0 0 0 -3-3h-61.19a3 3 0 1 0 0 5.92h61.29a2.88 2.88 0 0 0 2.9-2.97zm-121.04 10.11h28.42a22.33 22.33 0 1 0 -28.42 0z" fill="#dbdbdb"/><path d="m0 245.48h292v74.52h-292z" fill="url(#c)"/><path d="m56.25 95.08a33.36 33.36 0 1 1 33.36-33.36 33.4 33.4 0 0 1 -33.36 33.36zm0-60.72a27.36 27.36 0 1 0 27.36 27.36 27.4 27.4 0 0 0 -27.36-27.36z" fill="#2c6bed"/></svg>
|
After Width: | Height: | Size: 3.9 KiB |
|
@ -14,7 +14,7 @@
|
|||
max-width: 420px;
|
||||
}
|
||||
|
||||
&__body {
|
||||
&__contents {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
@ -110,5 +110,17 @@
|
|||
&__description {
|
||||
@include font-body-1;
|
||||
@include fixed-height(5.5em);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
&__instructions-button {
|
||||
width: 100%;
|
||||
&--hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.BadgeCarouselIndex {
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
|
|
52
stylesheets/components/BadgeSustainerInstructionsDialog.scss
Normal file
52
stylesheets/components/BadgeSustainerInstructionsDialog.scss
Normal file
|
@ -0,0 +1,52 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.BadgeSustainerInstructionsDialog {
|
||||
user-select: none;
|
||||
|
||||
// We use this selector for specificity.
|
||||
&.module-Modal {
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
&__header {
|
||||
@include font-title-2;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__subheader {
|
||||
@include font-body-1;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&__instructions {
|
||||
@include font-body-2;
|
||||
padding: 0;
|
||||
list-style-position: inside;
|
||||
|
||||
&::before {
|
||||
background-size: contain;
|
||||
content: '';
|
||||
display: block;
|
||||
height: 160px;
|
||||
margin: 24px auto;
|
||||
width: 146px;
|
||||
|
||||
@include light-theme {
|
||||
background-image: url('../images/mobile-settings-light.svg');
|
||||
}
|
||||
@include dark-theme {
|
||||
background-image: url('../images/mobile-settings-dark.svg');
|
||||
}
|
||||
}
|
||||
|
||||
> li {
|
||||
margin-top: 1em;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -35,6 +35,11 @@
|
|||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&--large {
|
||||
@include font-title-2;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&--medium {
|
||||
@include font-body-1-bold;
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
@import './components/AvatarTextEditor.scss';
|
||||
@import './components/BadgeCarouselIndex.scss';
|
||||
@import './components/BadgeDialog.scss';
|
||||
@import './components/BadgeSustainerInstructionsDialog.scss';
|
||||
@import './components/BetterAvatarBubble.scss';
|
||||
@import './components/Button.scss';
|
||||
@import './components/CallingLobby.scss';
|
||||
|
|
|
@ -2,14 +2,18 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { strictAssert } from '../util/assert';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import type { BadgeType } from '../badges/types';
|
||||
import { BadgeCategory } from '../badges/BadgeCategory';
|
||||
import { Modal } from './Modal';
|
||||
import { Button, ButtonSize } from './Button';
|
||||
import { BadgeDescription } from './BadgeDescription';
|
||||
import { BadgeImage } from './BadgeImage';
|
||||
import { BadgeCarouselIndex } from './BadgeCarouselIndex';
|
||||
import { BadgeSustainerInstructionsDialog } from './BadgeSustainerInstructionsDialog';
|
||||
|
||||
type PropsType = Readonly<{
|
||||
badges: ReadonlyArray<BadgeType>;
|
||||
|
@ -20,16 +24,32 @@ type PropsType = Readonly<{
|
|||
}>;
|
||||
|
||||
export function BadgeDialog(props: PropsType): null | JSX.Element {
|
||||
const { badges, onClose } = props;
|
||||
const { badges, i18n, onClose } = props;
|
||||
|
||||
const [isShowingInstructions, setIsShowingInstructions] = useState(false);
|
||||
|
||||
const hasBadges = badges.length > 0;
|
||||
useEffect(() => {
|
||||
if (!hasBadges) {
|
||||
if (!hasBadges && !isShowingInstructions) {
|
||||
onClose();
|
||||
}
|
||||
}, [hasBadges, onClose]);
|
||||
}, [hasBadges, isShowingInstructions, onClose]);
|
||||
|
||||
return hasBadges ? <BadgeDialogWithBadges {...props} /> : null;
|
||||
if (isShowingInstructions) {
|
||||
return (
|
||||
<BadgeSustainerInstructionsDialog
|
||||
i18n={i18n}
|
||||
onClose={() => setIsShowingInstructions(false)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return hasBadges ? (
|
||||
<BadgeDialogWithBadges
|
||||
{...props}
|
||||
onShowInstructions={() => setIsShowingInstructions(true)}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
|
||||
function BadgeDialogWithBadges({
|
||||
|
@ -37,8 +57,9 @@ function BadgeDialogWithBadges({
|
|||
firstName,
|
||||
i18n,
|
||||
onClose,
|
||||
onShowInstructions,
|
||||
title,
|
||||
}: PropsType): JSX.Element {
|
||||
}: PropsType & { onShowInstructions: () => unknown }): JSX.Element {
|
||||
const firstBadge = badges[0];
|
||||
strictAssert(
|
||||
firstBadge,
|
||||
|
@ -75,35 +96,48 @@ function BadgeDialogWithBadges({
|
|||
i18n={i18n}
|
||||
onClose={onClose}
|
||||
>
|
||||
<button
|
||||
aria-label={i18n('previous')}
|
||||
className="BadgeDialog__nav BadgeDialog__nav--previous"
|
||||
disabled={currentBadgeIndex === 0}
|
||||
onClick={() => navigate(-1)}
|
||||
type="button"
|
||||
/>
|
||||
<div className="BadgeDialog__main">
|
||||
<BadgeImage badge={currentBadge} size={160} />
|
||||
<div className="BadgeDialog__name">{currentBadge.name}</div>
|
||||
<div className="BadgeDialog__description">
|
||||
<BadgeDescription
|
||||
firstName={firstName}
|
||||
template={currentBadge.descriptionTemplate}
|
||||
title={title}
|
||||
<div className="BadgeDialog__contents">
|
||||
<button
|
||||
aria-label={i18n('previous')}
|
||||
className="BadgeDialog__nav BadgeDialog__nav--previous"
|
||||
disabled={currentBadgeIndex === 0}
|
||||
onClick={() => navigate(-1)}
|
||||
type="button"
|
||||
/>
|
||||
<div className="BadgeDialog__main">
|
||||
<BadgeImage badge={currentBadge} size={160} />
|
||||
<div className="BadgeDialog__name">{currentBadge.name}</div>
|
||||
<div className="BadgeDialog__description">
|
||||
<BadgeDescription
|
||||
firstName={firstName}
|
||||
template={currentBadge.descriptionTemplate}
|
||||
title={title}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className={classNames(
|
||||
'BadgeDialog__instructions-button',
|
||||
currentBadge.category !== BadgeCategory.Donor &&
|
||||
'BadgeDialog__instructions-button--hidden'
|
||||
)}
|
||||
onClick={onShowInstructions}
|
||||
size={ButtonSize.Large}
|
||||
>
|
||||
{i18n('BadgeDialog__become-a-sustainer-button')}
|
||||
</Button>
|
||||
<BadgeCarouselIndex
|
||||
currentIndex={currentBadgeIndex}
|
||||
totalCount={badges.length}
|
||||
/>
|
||||
</div>
|
||||
<BadgeCarouselIndex
|
||||
currentIndex={currentBadgeIndex}
|
||||
totalCount={badges.length}
|
||||
<button
|
||||
aria-label={i18n('next')}
|
||||
className="BadgeDialog__nav BadgeDialog__nav--next"
|
||||
disabled={currentBadgeIndex === badges.length - 1}
|
||||
onClick={() => navigate(1)}
|
||||
type="button"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
aria-label={i18n('next')}
|
||||
className="BadgeDialog__nav BadgeDialog__nav--next"
|
||||
disabled={currentBadgeIndex === badges.length - 1}
|
||||
onClick={() => navigate(1)}
|
||||
type="button"
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
33
ts/components/BadgeSustainerInstructionsDialog.tsx
Normal file
33
ts/components/BadgeSustainerInstructionsDialog.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { Modal } from './Modal';
|
||||
|
||||
export function BadgeSustainerInstructionsDialog({
|
||||
i18n,
|
||||
onClose,
|
||||
}: Readonly<{ i18n: LocalizerType; onClose: () => unknown }>): ReactElement {
|
||||
return (
|
||||
<Modal
|
||||
hasXButton
|
||||
moduleClassName="BadgeSustainerInstructionsDialog"
|
||||
i18n={i18n}
|
||||
onClose={onClose}
|
||||
>
|
||||
<h1 className="BadgeSustainerInstructionsDialog__header">
|
||||
{i18n('BadgeSustainerInstructions__header')}
|
||||
</h1>
|
||||
<h2 className="BadgeSustainerInstructionsDialog__subheader">
|
||||
{i18n('BadgeSustainerInstructions__subheader')}
|
||||
</h2>
|
||||
<ol className="BadgeSustainerInstructionsDialog__instructions">
|
||||
<li>{i18n('BadgeSustainerInstructions__instructions__1')}</li>
|
||||
<li>{i18n('BadgeSustainerInstructions__instructions__2')}</li>
|
||||
<li>{i18n('BadgeSustainerInstructions__instructions__3')}</li>
|
||||
</ol>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -13,7 +13,7 @@ story.add('Kitchen sink', () => (
|
|||
<>
|
||||
{Object.values(ButtonVariant).map(variant => (
|
||||
<React.Fragment key={variant}>
|
||||
{[ButtonSize.Medium, ButtonSize.Small].map(size => (
|
||||
{[ButtonSize.Large, ButtonSize.Medium, ButtonSize.Small].map(size => (
|
||||
<React.Fragment key={size}>
|
||||
<p>
|
||||
<Button onClick={action('onClick')} size={size} variant={variant}>
|
||||
|
|
|
@ -8,6 +8,7 @@ import classNames from 'classnames';
|
|||
import { assert } from '../util/assert';
|
||||
|
||||
export enum ButtonSize {
|
||||
Large,
|
||||
Medium,
|
||||
Small,
|
||||
}
|
||||
|
@ -65,6 +66,7 @@ type PropsType = {
|
|||
);
|
||||
|
||||
const SIZE_CLASS_NAMES = new Map<ButtonSize, string>([
|
||||
[ButtonSize.Large, 'module-Button--large'],
|
||||
[ButtonSize.Medium, 'module-Button--medium'],
|
||||
[ButtonSize.Small, 'module-Button--small'],
|
||||
]);
|
||||
|
|
Loading…
Add table
Reference in a new issue