Skip to content

Commit 009f0e3

Browse files
authored
Merge pull request #406 from PretendoNetwork/work/shot-final-stretch
feat(ctr): image upload support
2 parents 038ffe8 + a47f6c3 commit 009f0e3

File tree

19 files changed

+209
-77
lines changed

19 files changed

+209
-77
lines changed

apps/juxtaposition-ui/src/images.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,12 +220,14 @@ export type ScreenshotUrls = {
220220
};
221221

222222
export type UploadScreenshotOptions = {
223+
buffer?: Buffer;
223224
blob: string;
224225
pid: number;
225226
postId: string;
226227
};
227228
export async function uploadScreenshot(opts: UploadScreenshotOptions): Promise<ScreenshotUrls | null> {
228-
const screenshotBuf = Buffer.from(opts.blob.replace(/\0/g, '').trim(), 'base64');
229+
// try buffer, otherwise blob
230+
const screenshotBuf = opts.buffer ?? Buffer.from(opts.blob.replace(/\0/g, '').trim(), 'base64');
229231
const screenshots = processJpgScreenshot(screenshotBuf);
230232
if (screenshots === null) {
231233
return null;

apps/juxtaposition-ui/src/services/juxt-web/routes/console/communities.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,8 @@ communitiesRouter.get('/:communityID/create', async function (req, res) {
135135
name: community.name,
136136
url: `/posts/new`,
137137
show: 'post',
138-
shotMode
138+
shotMode,
139+
community
139140
};
140141
res.jsxForDirectory({
141142
ctr: <CtrNewPostPage {...props} />,

apps/juxtaposition-ui/src/services/juxt-web/routes/console/posts.tsx

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ import type { PaintingUrls } from '@/images';
2828
import type { PostPageViewProps } from '@/services/juxt-web/views/web/postPageView';
2929
import type { HydratedSettingsDocument } from '@/models/settings';
3030
import type { ContentSchema } from '@/models/content';
31-
const upload = multer({ dest: 'uploads/' });
31+
const storage = multer.memoryStorage();
32+
const upload = multer({ storage });
3233
export const postsRouter = express.Router();
3334

3435
const postLimit = rateLimit({
@@ -136,7 +137,7 @@ postsRouter.post('/empathy', yeahLimit, async function (req, res) {
136137
await redisRemove(`${post.pid}_user_page_posts`);
137138
});
138139

139-
postsRouter.post('/new', postLimit, upload.none(), async function (req, res) {
140+
postsRouter.post('/new', postLimit, upload.fields([{ name: 'shot', maxCount: 1 }]), async function (req, res) {
140141
await newPost(req, res);
141142
});
142143

@@ -242,7 +243,7 @@ postsRouter.delete('/:post_id', async function (req, res) {
242243
await redisRemove(`${post.pid}_user_page_posts`);
243244
});
244245

245-
postsRouter.post('/:post_id/new', postLimit, upload.none(), async function (req, res) {
246+
postsRouter.post('/:post_id/new', postLimit, upload.fields([{ name: 'shot', maxCount: 1 }]), async function (req, res) {
246247
await newPost(req, res);
247248
});
248249

@@ -270,7 +271,8 @@ postsRouter.get('/:post_id/create', async function (req, res) {
270271
pid: parent.pid,
271272
url: `/posts/${parent.id}/new`,
272273
show: 'post',
273-
shotMode
274+
shotMode,
275+
community
274276
};
275277
res.jsxForDirectory({
276278
ctr: <CtrNewPostPage {...props} />,
@@ -335,7 +337,7 @@ postsRouter.post('/:post_id/report', upload.none(), async function (req, res) {
335337
});
336338

337339
async function newPost(req: Request, res: Response): Promise<void> {
338-
const { params, body, auth } = parseReq(req, {
340+
const { params, body, files, auth } = parseReq(req, {
339341
params: z.object({
340342
post_id: z.string().optional()
341343
}),
@@ -350,7 +352,8 @@ async function newPost(req: Request, res: Response): Promise<void> {
350352
spoiler: z.stringbool().default(false),
351353
is_app_jumpable: z.stringbool().default(false),
352354
language_id: z.coerce.number().optional()
353-
})
355+
}),
356+
files: ['shot']
354357
});
355358

356359
const userSettings = await database.getUserSettings(auth().pid);
@@ -370,7 +373,7 @@ async function newPost(req: Request, res: Response): Promise<void> {
370373
}
371374
}
372375
}
373-
if (params.post_id && (body.body === '' && body.painting === '' && body.screenshot === '')) {
376+
if (params.post_id && (body.body === '' && body.painting === '' && body.screenshot === '' && files.shot.length == 0)) {
374377
res.status(422);
375378
return res.redirect('/posts/' + req.params.post_id.toString());
376379
}
@@ -404,8 +407,10 @@ async function newPost(req: Request, res: Response): Promise<void> {
404407
}
405408
}
406409
let screenshots = null;
407-
if (body.screenshot && getShotMode(community, auth().paramPackData) !== 'block') {
410+
if ((body.screenshot || files.shot.length === 1) &&
411+
getShotMode(community, auth().paramPackData) !== 'block') {
408412
screenshots = await uploadScreenshot({
413+
buffer: files.shot[0]?.buffer,
409414
blob: body.screenshot,
410415
pid: auth().pid,
411416
postId

apps/juxtaposition-ui/src/services/juxt-web/views/ctr/communityView.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,19 @@ export function CtrCommunityView(props: CommunityViewProps): ReactNode {
7676
<button
7777
type="button"
7878
className={cx('small-button follow', {
79-
suggested: props.hasSubCommunities,
80-
selected: props.isUserFollowing
79+
suggested: props.hasSubCommunities
80+
8181
})}
8282
evt-click="follow(this)"
8383
data-sound="SE_WAVE_CHECKBOX_UNCHECK"
8484
data-url="/titles/follow"
8585
data-community-id={community.olive_community_id}
8686
>
87-
<span className="sprite sp-yeah inline-sprite"></span>
87+
<span className={cx('sprite sp-yeah inline-sprite', {
88+
selected: props.isUserFollowing
89+
})}
90+
>
91+
</span>
8892
</button>
8993
)
9094
: null}

apps/juxtaposition-ui/src/services/juxt-web/views/ctr/newPostView.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,17 +41,17 @@ export function CtrNewPostView(props: NewPostViewProps): ReactNode {
4141
const url = useUrl();
4242
const user = useUser();
4343
const cache = useCache();
44-
const { ctrBanner, ctrLegacy } = props;
44+
const { bannerUrl, legacy } = props.community ? url.ctrHeader(props.community) : {};
4545
const name = props.name ?? cache.getUserName(props.pid ?? 0);
4646
return (
4747
<div id="add-post-page" className="add-post-page official-user-post">
4848
<header
4949
id="header"
5050
style={{
51-
background: ctrBanner ? `url('${ctrBanner}')` : ''
51+
background: bannerUrl ? `url('${bannerUrl}')` : ''
5252
}}
5353
className={cx(
54-
{ 'header-legacy': ctrLegacy }
54+
{ 'header-legacy': legacy }
5555
)}
5656

5757
data-toolbar-mode="wide"
@@ -61,7 +61,7 @@ export function CtrNewPostView(props: NewPostViewProps): ReactNode {
6161
<T k="new_post.post_to" values={{ user: name ?? '' }} />
6262
</h1>
6363
</header>
64-
<form method="post" action={props.url} id="posts-form" data-is-own-title="1" data-is-identified="1">
64+
<form method="post" action={props.url} id="posts-form" data-is-own-title="1" data-is-identified="1" encType="multipart/form-data">
6565
<input type="hidden" name="community_id" value={props.id} />
6666
<input type="hidden" name="bmp" value="true" />
6767
<div className="add-post-page-content">
@@ -93,7 +93,18 @@ export function CtrNewPostView(props: NewPostViewProps): ReactNode {
9393
{props.shotMode !== 'block'
9494
? (
9595
<CtrTabView name="_post_type" value="shot" sprite="sp-shot-input" data-shot-mode={props.shotMode}>
96-
<div id="shot-msg">Screenshots are not ready yet. Check back soon!</div>
96+
<div id="shot-preview" data-shot-preview="1"></div>
97+
98+
<div className="shot-picker">
99+
<input type="radio" name="shot-type" className="shot top" data-shot="1" data-lls="shot-top" />
100+
<input type="radio" name="shot-type" className="shot btm" data-shot="0" data-lls="shot-btm" />
101+
<div id="shot-clear">
102+
<div className="sprite sp-clear centred" />
103+
<input type="radio" name="shot-type" data-shot-clear="1" />
104+
</div>
105+
</div>
106+
107+
<input type="file" name="shot" data-shot-upload="1" disabled />
97108
</CtrTabView>
98109
)
99110
: null }

apps/juxtaposition-ui/src/services/juxt-web/views/ctr/notificationListView.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ function CtrNotificationItem(props: NotificationItemProps): ReactNode {
3131
<CtrMiiIcon pid={Number(notif.objectID)} type="icon"></CtrMiiIcon>
3232
<div className="body">
3333
<p>
34-
<a className="link" href={notif.link ?? '#'}>
34+
<a className="link" href={notif.link ?? '#'} data-pjax="#body">
3535
<T
3636
k={i18nKey}
3737
values={{
@@ -59,7 +59,7 @@ function CtrNotificationItem(props: NotificationItemProps): ReactNode {
5959
<>
6060
<CtrIcon href={notif.link ?? undefined} src={notif.image ?? ''}></CtrIcon>
6161
<div className="body">
62-
<a href={notif.link ?? undefined}>
62+
<a href={notif.link ?? undefined} data-pjax="#body">
6363
<p style={{ color: 'black' }}>
6464
<span>{notif.text}</span>
6565
<span className="timestamp">

apps/juxtaposition-ui/src/services/juxt-web/views/ctr/userPageView.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ export function CtrUserPageView(props: UserPageViewProps): ReactNode {
7373
{isSelf ? <a id="header-communities-button" className="header-button left" href="/users/me/settings" data-pjax="#body"><T k="user_page.settings" /></a> : null}
7474
{ canViewUser && !isSelf
7575
? (
76-
<button type="button" className={cx('small-button follow', { selected: isRequesterFollowingUser })} evt-click="follow(this)" data-sound="SE_WAVE_CHECKBOX_UNCHECK" data-url="/users/follow" data-community-id={props.user.pid}>
77-
<span className="sprite sp-yeah inline-sprite"></span>
76+
<button type="button" className="small-button follow" evt-click="follow(this)" data-sound="SE_WAVE_CHECKBOX_UNCHECK" data-url="/users/follow" data-community-id={props.user.pid}>
77+
<span className={cx('sprite sp-yeah inline-sprite', { selected: isRequesterFollowingUser })}></span>
7878
</button>
7979
)
8080
: null}

apps/juxtaposition-ui/src/services/juxt-web/views/web/newPostView.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { T } from '@/services/juxt-web/views/common/components/T';
22
import { useUrl } from '@/services/juxt-web/views/common/hooks/useUrl';
33
import { useUser } from '@/services/juxt-web/views/common/hooks/useUser';
4+
import type { InferSchemaType } from 'mongoose';
45
import type { ReactNode } from 'react';
5-
import type { CommunityShotMode } from '@/models/communities';
6+
import type { CommunitySchema, CommunityShotMode } from '@/models/communities';
67

78
const empathies = [
89
{
@@ -51,11 +52,10 @@ export type NewPostViewProps = {
5152
pid?: number;
5253
url: string;
5354
show: string;
55+
// must provide messagePid OR community
5456
messagePid?: number;
57+
community?: InferSchemaType<typeof CommunitySchema>;
5558
shotMode: CommunityShotMode;
56-
// ctr only
57-
ctrBanner?: string;
58-
ctrLegacy?: boolean;
5959
};
6060

6161
export function WebNewPostView(props: NewPostViewProps): ReactNode {

apps/juxtaposition-ui/webfiles/ctr/css/new-post-view.css

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,83 @@
9797
height: 100%;
9898
}
9999

100-
#shot-msg {
101-
padding: 5px;
100+
#shot-preview {
101+
float: right;
102+
width: 200px;
103+
height: 120px;
104+
105+
background-color: lightgray;
106+
background-size: contain;
107+
background-repeat: no-repeat;
108+
background-position: center;
109+
border: 0;
110+
}
111+
112+
.shot-picker {
113+
display: inline-block;
114+
padding: 4px;
115+
margin: 5px 14px;
116+
border: 1px solid gray;
117+
118+
position: relative;
119+
border-radius: 2px;
120+
121+
input {
122+
appearance: none;
123+
display: block;
124+
padding: 0;
125+
margin: 0;
126+
cursor: pointer;
127+
}
128+
129+
.shot {
130+
margin: auto;
131+
border: 1px solid white;
132+
border-radius: 2px;
133+
134+
/* background-image set via script */
135+
background-size: contain;
136+
137+
height: 50px; /* 48 + border */
138+
&.top {
139+
/* 400x240px -> 80px + border */
140+
width: 82px;
141+
}
142+
&.btm {
143+
/* 320x240px -> 64 + border*/
144+
width: 66px;
145+
}
146+
&:checked {
147+
border: 1px solid #6b3cb5;
148+
}
149+
}
150+
151+
#shot-clear {
152+
width: 17px;
153+
height: 17px;
154+
position: absolute;
155+
bottom: -4px; /* ^^; */
156+
right: -9px;
157+
158+
border-radius: 2px;
159+
border: 1px solid gray;
160+
background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#dfdfdf)) 0 0;
161+
input {
162+
width: 100%;
163+
height: 100%;
164+
}
165+
}
166+
}
167+
168+
/* Display: none makes it stop taking focus.. so instead just kinda hide it */
169+
input[type="file"] {
170+
position: absolute;
171+
top: 0px;
172+
opacity: 0;
173+
/* Small enough to fit */
174+
width: 10px;
175+
height: 10px;
176+
pointer-events: none;
102177
}
103178

104179
#memo-img-input {
11.2 KB
Loading

0 commit comments

Comments
 (0)