Big refactor: ts/ directory for all typescript, including react
Split out test-specific and general utility react components too. And moved our test/legacy* files for the Style Guide into a styleguide/ subdirectory of test/. I think we'll be able to live in this directory structure for a while.
This commit is contained in:
parent
50d4dbaae7
commit
23537546fe
18 changed files with 54 additions and 37 deletions
6
ts/components/conversation/Message.md
Normal file
6
ts/components/conversation/Message.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<Message />
|
||||
</util.ConversationContext>
|
||||
```
|
36
ts/components/conversation/Message.tsx
Normal file
36
ts/components/conversation/Message.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import React from 'react';
|
||||
|
||||
|
||||
/**
|
||||
* A placeholder Message component, giving the structure of a plain message with none of
|
||||
* the dynamic functionality. We can build off of this going forward.
|
||||
*/
|
||||
export class Message extends React.Component<{}, {}> {
|
||||
public render() {
|
||||
return (
|
||||
<li className="entry outgoing sent delivered">
|
||||
<span className="avatar" />
|
||||
<div className="bubble">
|
||||
<div className="sender" dir="auto" />
|
||||
<div className="attachments" />
|
||||
<p className="content" dir="auto">
|
||||
<span className="body">
|
||||
Hi there. How are you doing? Feeling pretty good? Awesome.
|
||||
</span>
|
||||
</p>
|
||||
<div className="meta">
|
||||
<span
|
||||
className="timestamp"
|
||||
data-timestamp="1522800995425"
|
||||
title="Tue, Apr 3, 2018 5:16 PM"
|
||||
>
|
||||
1 minute ago
|
||||
</span>
|
||||
<span className="status hide" />
|
||||
<span className="timer" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
2
ts/components/conversation/Reply.md
Normal file
2
ts/components/conversation/Reply.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
|
||||
This is Reply.md.
|
14
ts/components/conversation/Reply.tsx
Normal file
14
ts/components/conversation/Reply.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
|
||||
|
||||
interface Props { name: string; }
|
||||
|
||||
interface State { count: number; }
|
||||
|
||||
export class Reply extends React.Component<Props, State> {
|
||||
public render() {
|
||||
return (
|
||||
<div>Placeholder</div>
|
||||
);
|
||||
}
|
||||
}
|
20
ts/components/utility/BackboneWrapper.md
Normal file
20
ts/components/utility/BackboneWrapper.md
Normal file
|
@ -0,0 +1,20 @@
|
|||
Rendering a real `Whisper.MessageView` using `<util.ConversationContext />` and
|
||||
`<util.BackboneWrapper />`.
|
||||
|
||||
```jsx
|
||||
const model = new Whisper.Message({
|
||||
type: 'outgoing',
|
||||
body: 'text',
|
||||
sent_at: Date.now() - 5000,
|
||||
})
|
||||
const View = Whisper.MessageView;
|
||||
const options = {
|
||||
model,
|
||||
};
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<util.BackboneWrapper
|
||||
View={View}
|
||||
options={options}
|
||||
/>
|
||||
</util.ConversationContext>
|
||||
```
|
69
ts/components/utility/BackboneWrapper.tsx
Normal file
69
ts/components/utility/BackboneWrapper.tsx
Normal file
|
@ -0,0 +1,69 @@
|
|||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
/** The View class, which will be instantiated then treated like a Backbone View */
|
||||
readonly View: BackboneViewConstructor;
|
||||
/** Options to be passed along to the view when constructed */
|
||||
readonly options: object;
|
||||
}
|
||||
|
||||
interface BackboneView {
|
||||
remove: () => void;
|
||||
render: () => void;
|
||||
el: HTMLElement;
|
||||
}
|
||||
|
||||
interface BackboneViewConstructor {
|
||||
new (options: object): BackboneView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows Backbone Views to be rendered inside of React (primarily for the Style Guide)
|
||||
* while we slowly replace the internals of a given Backbone view with React.
|
||||
*/
|
||||
export class BackboneWrapper extends React.Component<Props, {}> {
|
||||
protected el: Element | null = null;
|
||||
protected view: BackboneView | null = null;
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.teardown();
|
||||
}
|
||||
|
||||
public shouldComponentUpdate() {
|
||||
// we're handling all updates manually
|
||||
return false;
|
||||
}
|
||||
|
||||
public render() {
|
||||
return <div ref={this.setEl} />;
|
||||
}
|
||||
|
||||
protected setEl = (element: HTMLDivElement | null) => {
|
||||
this.el = element;
|
||||
this.setup();
|
||||
}
|
||||
|
||||
protected setup = () => {
|
||||
const { el } = this;
|
||||
const { View, options } = this.props;
|
||||
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
this.view = new View(options);
|
||||
this.view.render();
|
||||
|
||||
// It's important to let the view create its own root DOM element. This ensures that
|
||||
// its tagName property actually takes effect.
|
||||
el.appendChild(this.view.el);
|
||||
}
|
||||
|
||||
protected teardown() {
|
||||
if (!this.view) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.view.remove();
|
||||
this.view = null;
|
||||
}
|
||||
}
|
8
ts/test/ConversationContext.md
Normal file
8
ts/test/ConversationContext.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
|
||||
The simplest example of using the `<ConversationContext />` component:
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<div>Just a plain bit of text</div>
|
||||
</util.ConversationContext>
|
||||
```
|
31
ts/test/ConversationContext.tsx
Normal file
31
ts/test/ConversationContext.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* Corresponds to the theme setting in the app, and the class added to the root element.
|
||||
*/
|
||||
theme: 'ios' | 'android' | 'android-dark';
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the parent elements necessary to allow the main Signal Desktop stylesheet to
|
||||
* apply (with no changes) to messages in the Style Guide.
|
||||
*/
|
||||
export class ConversationContext extends React.Component<Props, {}> {
|
||||
public render() {
|
||||
const { theme } = this.props;
|
||||
|
||||
return (
|
||||
<div className={theme}>
|
||||
<div className="conversation">
|
||||
<div className="discussion-container" style={{padding: '0.5em'}}>
|
||||
<ul className="message-list">
|
||||
{this.props.children}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
83
ts/test/StyleGuideUtil.ts
Normal file
83
ts/test/StyleGuideUtil.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
import moment from 'moment';
|
||||
import qs from 'qs';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
|
||||
// Helper components used in the Style Guide, exposed at 'util' in the global scope via
|
||||
// the 'context' option in react-styleguidist.
|
||||
|
||||
export { ConversationContext } from './ConversationContext';
|
||||
export { BackboneWrapper } from '../components/utility/BackboneWrapper';
|
||||
|
||||
// Here we can make things inside Webpack available to Backbone views like preload.js.
|
||||
|
||||
import { Message } from '../components/conversation/Message';
|
||||
import { Reply } from '../components/conversation/Reply';
|
||||
|
||||
|
||||
// TypeScript wants two things when you import:
|
||||
// 1) a normal typescript file
|
||||
// 2) a javascript file with type definitions
|
||||
// Anything else will raise an error, that it can't find the module. And so, we ignore...
|
||||
|
||||
// @ts-ignore
|
||||
import gif from '../../fixtures/giphy-GVNvOUpeYmI7e.gif';
|
||||
// @ts-ignore
|
||||
import mp3 from '../../fixtures/incompetech-com-Agnus-Dei-X.mp3';
|
||||
// @ts-ignore
|
||||
import txt from '../../fixtures/lorem-ipsum.txt';
|
||||
// @ts-ignore
|
||||
import mp4 from '../../fixtures/pixabay-Soap-Bubble-7141.mp4';
|
||||
|
||||
export {
|
||||
mp3,
|
||||
gif,
|
||||
mp4,
|
||||
txt,
|
||||
};
|
||||
|
||||
|
||||
// Required, or TypeScript complains about adding keys to window
|
||||
const parent = window as any;
|
||||
|
||||
const query = window.location.search.replace(/^\?/, '');
|
||||
const urlOptions = qs.parse(query);
|
||||
const theme = urlOptions.theme || 'android';
|
||||
const locale = urlOptions.locale || 'en';
|
||||
|
||||
// @ts-ignore
|
||||
import localeMessages from '../../_locales/en/messages.json';
|
||||
|
||||
// @ts-ignore
|
||||
import { setup } from '../../js/modules/i18n';
|
||||
|
||||
const i18n = setup(locale, localeMessages);
|
||||
|
||||
export {
|
||||
theme,
|
||||
locale,
|
||||
i18n,
|
||||
};
|
||||
|
||||
|
||||
parent.i18n = i18n;
|
||||
parent.moment = moment;
|
||||
|
||||
parent.moment.updateLocale(locale, {
|
||||
relativeTime: {
|
||||
h: parent.i18n('timestamp_h'),
|
||||
m: parent.i18n('timestamp_m'),
|
||||
s: parent.i18n('timestamp_s'),
|
||||
},
|
||||
});
|
||||
parent.moment.locale(locale);
|
||||
|
||||
parent.React = React;
|
||||
parent.ReactDOM = ReactDOM;
|
||||
|
||||
const SignalReact = parent.Signal.React = parent.Signal.React || {};
|
||||
|
||||
SignalReact.Message = Message;
|
||||
SignalReact.Reply = Reply;
|
Loading…
Add table
Add a link
Reference in a new issue