Full styleguide now available via yarn styleguide
Due to a number of hacks, the style guide can be used to show Backbone views. This will allow a smooth path from the old way of doing things to the new.
This commit is contained in:
parent
893fb1cb9e
commit
1326b26585
21 changed files with 4006 additions and 363 deletions
|
@ -26,6 +26,7 @@ js/react/**/*.js
|
|||
!js/models/conversations.js
|
||||
!js/views/attachment_view.js
|
||||
!js/views/conversation_search_view.js
|
||||
!js/views/backbone_wrapper_view.js
|
||||
!js/views/debug_log_view.js
|
||||
!js/views/file_input_view.js
|
||||
!js/views/inbox_view.js
|
||||
|
|
|
@ -13,7 +13,6 @@ assets
|
|||
|
||||
# examples
|
||||
example
|
||||
examples
|
||||
|
||||
# code coverage directories
|
||||
coverage
|
||||
|
|
|
@ -910,6 +910,7 @@
|
|||
<script type='text/javascript' src='js/conversation_controller.js'></script>
|
||||
<script type='text/javascript' src='js/emoji_util.js'></script>
|
||||
|
||||
<script type='text/javascript' src='js/views/backbone_wrapper_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/whisper_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/last_seen_indicator_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/scroll_down_button_view.js'></script>
|
||||
|
|
6
js/react/conversation/Message.md
Normal file
6
js/react/conversation/Message.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
```jsx
|
||||
<util.MessageParents theme="android">
|
||||
<Message />
|
||||
</util.MessageParents>
|
||||
```
|
36
js/react/conversation/Message.tsx
Normal file
36
js/react/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
js/react/conversation/Reply.md
Normal file
2
js/react/conversation/Reply.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
|
||||
This is Reply.md.
|
14
js/react/conversation/Reply.tsx
Normal file
14
js/react/conversation/Reply.tsx
Normal file
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
|
||||
|
||||
interface IProps { name: string; }
|
||||
|
||||
interface IState { count: number; }
|
||||
|
||||
export class Reply extends React.Component<IProps, IState> {
|
||||
public render() {
|
||||
return (
|
||||
<div>Placeholder</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
interface IProps { name: string; }
|
||||
|
||||
interface IState { count: number; }
|
||||
|
||||
|
||||
const items = [
|
||||
'one',
|
||||
'two',
|
||||
'three',
|
||||
'four',
|
||||
];
|
||||
|
||||
export class InlineReply extends React.Component<IProps, IState> {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
count: 0,
|
||||
};
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { name } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
This is a basic component. Hi there, {name}!
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function greeter2(person: any) {
|
||||
// console.log(items);
|
||||
return `Hello, ${person}`;
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import { InlineReply } from './sub/test2';
|
||||
|
||||
// console.log(InlineReply);
|
||||
|
||||
export function greeter(person: any) {
|
||||
return 'Hello, ' + person;
|
||||
}
|
20
js/react/util/BackboneWrapper.md
Normal file
20
js/react/util/BackboneWrapper.md
Normal file
|
@ -0,0 +1,20 @@
|
|||
Rendering a real `Whisper.MessageView` using `<util.MessageParents />` 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.MessageParents theme="android">
|
||||
<util.BackboneWrapper
|
||||
View={View}
|
||||
options={options}
|
||||
/>
|
||||
</util.MessageParents>
|
||||
```
|
78
js/react/util/BackboneWrapper.tsx
Normal file
78
js/react/util/BackboneWrapper.tsx
Normal file
|
@ -0,0 +1,78 @@
|
|||
import React from 'react';
|
||||
|
||||
interface IProps {
|
||||
/** The View class, which will be instantiated then treated like a Backbone View */
|
||||
readonly View: IBackboneViewConstructor;
|
||||
/** Options to be passed along to the view when constructed */
|
||||
readonly options: object;
|
||||
}
|
||||
|
||||
interface IBackboneView {
|
||||
remove: () => void;
|
||||
render: () => void;
|
||||
el: HTMLElement;
|
||||
}
|
||||
|
||||
interface IBackboneViewConstructor {
|
||||
new (options: object): IBackboneView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows Backbone Views to be rendered inside of React (primarily for the styleguide)
|
||||
* while we slowly replace the internals of a given Backbone view with React.
|
||||
*/
|
||||
export class BackboneWrapper extends React.Component<IProps, {}> {
|
||||
protected el: Element | null;
|
||||
protected view: IBackboneView | null;
|
||||
protected setEl: (element: HTMLDivElement | null) => void;
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.el = null;
|
||||
this.view = null;
|
||||
|
||||
this.setEl = (element: HTMLDivElement | null) => {
|
||||
this.el = element;
|
||||
this.setup();
|
||||
};
|
||||
this.setup = this.setup.bind(this);
|
||||
}
|
||||
|
||||
public 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);
|
||||
}
|
||||
|
||||
public teardown() {
|
||||
if (!this.view) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.view.remove();
|
||||
this.view = null;
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.teardown();
|
||||
}
|
||||
|
||||
public shouldComponentUpdate() {
|
||||
// we're handling all updates manually
|
||||
return false;
|
||||
}
|
||||
|
||||
public render() {
|
||||
return <div ref={this.setEl} />;
|
||||
}
|
||||
}
|
8
js/react/util/MessageParents.md
Normal file
8
js/react/util/MessageParents.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
|
||||
The simplest example of using the `<MessagesParents />` component:
|
||||
|
||||
```jsx
|
||||
<util.MessageParents theme="android">
|
||||
<div>Just a plain bit of text</div>
|
||||
</util.MessageParents>
|
||||
```
|
31
js/react/util/MessageParents.tsx
Normal file
31
js/react/util/MessageParents.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
|
||||
|
||||
interface IProps {
|
||||
/**
|
||||
* 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 this context.
|
||||
*/
|
||||
export class MessageParents extends React.Component<IProps, {}> {
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
24
js/react/util/index.ts
Normal file
24
js/react/util/index.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
// Helper components used in the styleguide, exposed at 'util' in the global scope via the
|
||||
// context option in reaat-styleguidist.
|
||||
|
||||
export { MessageParents } from './MessageParents';
|
||||
export { BackboneWrapper } from './BackboneWrapper';
|
||||
|
||||
// Here we can make things inside Webpack available to Backbone views like preload.js.
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import { Message } from '../conversation/Message';
|
||||
import { Reply } from '../conversation/Reply';
|
||||
|
||||
// Required, or TypeScript complains about adding keys to window
|
||||
const parent = window as any;
|
||||
|
||||
parent.React = React;
|
||||
parent.ReactDOM = ReactDOM;
|
||||
|
||||
const SignalReact = parent.Signal.React = parent.Signal.React || {};
|
||||
|
||||
SignalReact.Message = Message;
|
||||
SignalReact.Reply = Reply;
|
47
js/views/backbone_wrapper_view.js
Normal file
47
js/views/backbone_wrapper_view.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
/* global Backbone: false */
|
||||
|
||||
// Additional globals used:
|
||||
// window.React
|
||||
// window.ReactDOM
|
||||
// window.i18n
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
window.Whisper.ReactWrapper = Backbone.View.extend({
|
||||
className: 'react-wrapper',
|
||||
initialize(options) {
|
||||
const { Component, props, onClose } = options;
|
||||
this.render();
|
||||
|
||||
this.Component = Component;
|
||||
this.onClose = onClose;
|
||||
|
||||
this.update(props);
|
||||
},
|
||||
update(props) {
|
||||
const updatedProps = this.augmentProps(props);
|
||||
const element = window.React.createElement(this.Component, updatedProps);
|
||||
window.ReactDOM.render(element, this.el);
|
||||
},
|
||||
augmentProps(props) {
|
||||
return Object.assign({}, props, {
|
||||
close: () => {
|
||||
if (this.onClose) {
|
||||
this.onClose();
|
||||
return;
|
||||
}
|
||||
this.remove();
|
||||
},
|
||||
i18n: window.i18n,
|
||||
});
|
||||
},
|
||||
remove() {
|
||||
window.ReactDOM.unmountComponentAtNode(this.el);
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
},
|
||||
});
|
||||
}());
|
12
package.json
12
package.json
|
@ -43,8 +43,9 @@
|
|||
"eslint": "eslint .",
|
||||
"tslint": "tslint ./js/react/**/*.{ts,tsx}",
|
||||
"transpile": "tsc",
|
||||
"clean-transpile": "rimraf js/built/",
|
||||
"open-coverage": "open coverage/lcov-report/index.html"
|
||||
"clean-transpile": "rimraf js/react/**/*.js js/react/*.js",
|
||||
"open-coverage": "open coverage/lcov-report/index.html",
|
||||
"styleguide": "styleguidist server"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sindresorhus/is": "^0.8.0",
|
||||
|
@ -92,6 +93,7 @@
|
|||
"devDependencies": {
|
||||
"@types/react": "^16.3.1",
|
||||
"@types/react-dom": "^16.0.4",
|
||||
"arraybuffer-loader": "^1.0.3",
|
||||
"asar": "^0.14.0",
|
||||
"bower": "^1.8.2",
|
||||
"chai": "^4.1.2",
|
||||
|
@ -120,11 +122,15 @@
|
|||
"node-sass-import-once": "^1.2.0",
|
||||
"nsp": "^3.2.1",
|
||||
"nyc": "^11.4.1",
|
||||
"react-docgen-typescript": "^1.2.6",
|
||||
"react-styleguidist": "^7.0.1",
|
||||
"sinon": "^4.4.2",
|
||||
"spectron": "^3.8.0",
|
||||
"ts-loader": "^4.1.0",
|
||||
"tslint": "^5.9.1",
|
||||
"tslint-react": "^3.5.1",
|
||||
"typescript": "^2.8.1"
|
||||
"typescript": "^2.8.1",
|
||||
"webpack": "^4.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^8.2.1"
|
||||
|
|
158
styleguide.config.js
Normal file
158
styleguide.config.js
Normal file
|
@ -0,0 +1,158 @@
|
|||
const webpack = require('webpack');
|
||||
const path = require('path');
|
||||
const typescriptSupport = require('react-docgen-typescript');
|
||||
|
||||
|
||||
const propsParser = typescriptSupport.withCustomConfig('./tsconfig.json').parse;
|
||||
|
||||
module.exports = {
|
||||
sections: [
|
||||
{
|
||||
name: 'Conversation',
|
||||
description: 'Everything necessary to render a conversation',
|
||||
components: 'js/react/conversation/*.tsx',
|
||||
},
|
||||
{
|
||||
name: 'Utility',
|
||||
description: 'Utility components only used for testing',
|
||||
components: 'js/react/util/*.tsx',
|
||||
},
|
||||
],
|
||||
context: {
|
||||
// Exposes necessary utilities in the global scope for all readme code snippets
|
||||
util: 'js/react/util',
|
||||
},
|
||||
// We don't want one long, single page
|
||||
pagePerSection: true,
|
||||
// Expose entire repository to the styleguidist server, primarily for stylesheets
|
||||
assetsDir: './',
|
||||
// Add top-level elements to the HTML:
|
||||
// docs: https://github.com/vxna/mini-html-webpack-template
|
||||
// https://react-styleguidist.js.org/docs/configuration.html#template
|
||||
template: {
|
||||
head: {
|
||||
links: [{
|
||||
rel: 'stylesheet',
|
||||
type: 'text/css',
|
||||
href: '/stylesheets/manifest.css',
|
||||
}],
|
||||
},
|
||||
body: {
|
||||
// Brings in all the necessary components to boostrap Backbone views
|
||||
// Mirrors the order used in background.js.
|
||||
scripts: [
|
||||
{
|
||||
src: 'test/legacy_bridge.js',
|
||||
},
|
||||
{
|
||||
src: 'node_modules/moment/min/moment-with-locales.min.js',
|
||||
},
|
||||
{
|
||||
src: 'js/components.js',
|
||||
},
|
||||
{
|
||||
src: 'js/reliable_trigger.js',
|
||||
},
|
||||
{
|
||||
src: 'js/database.js',
|
||||
},
|
||||
{
|
||||
src: 'js/storage.js',
|
||||
},
|
||||
{
|
||||
src: 'js/signal_protocol_store.js',
|
||||
},
|
||||
{
|
||||
src: 'js/libtextsecure.js',
|
||||
},
|
||||
{
|
||||
src: 'js/focus_listener.js',
|
||||
},
|
||||
{
|
||||
src: 'js/notifications.js',
|
||||
},
|
||||
{
|
||||
src: 'js/delivery_receipts.js',
|
||||
},
|
||||
{
|
||||
src: 'js/read_receipts.js',
|
||||
},
|
||||
{
|
||||
src: 'js/read_syncs.js',
|
||||
},
|
||||
{
|
||||
src: 'js/libphonenumber-util.js',
|
||||
},
|
||||
{
|
||||
src: 'js/models/messages.js',
|
||||
},
|
||||
{
|
||||
src: 'js/models/conversations.js',
|
||||
},
|
||||
{
|
||||
src: 'js/models/blockedNumbers.js',
|
||||
},
|
||||
{
|
||||
src: 'js/expiring_messages.js',
|
||||
},
|
||||
|
||||
{
|
||||
src: 'js/chromium.js',
|
||||
},
|
||||
{
|
||||
src: 'js/registration.js',
|
||||
},
|
||||
{
|
||||
src: 'js/expire.js',
|
||||
},
|
||||
{
|
||||
src: 'js/conversation_controller.js',
|
||||
},
|
||||
{
|
||||
src: 'js/emoji_util.js',
|
||||
},
|
||||
// Select Backbone views
|
||||
{
|
||||
src: 'js/views/whisper_view.js',
|
||||
},
|
||||
{
|
||||
src: 'js/views/timestamp_view.js',
|
||||
},
|
||||
{
|
||||
src: 'js/views/message_view.js',
|
||||
},
|
||||
// Hacky way of including templates for Backbone components
|
||||
{
|
||||
src: 'test/legacy_templates.js',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
propsParser,
|
||||
webpackConfig: {
|
||||
devtool: 'source-map',
|
||||
|
||||
resolve: {
|
||||
// Necessary to enable the absolute path used in the context option above
|
||||
modules: [
|
||||
__dirname,
|
||||
path.join(__dirname, 'node_modules'),
|
||||
],
|
||||
extensions: ['.tsx'],
|
||||
},
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
loader: 'ts-loader'
|
||||
},
|
||||
{
|
||||
// To test handling of attachments, we need arraybuffers in memory
|
||||
test: /\.(gif|mp3|mp4)$/,
|
||||
loader: 'arraybuffer-loader',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
|
@ -583,6 +583,7 @@
|
|||
|
||||
<script type="text/javascript" src="../js/chromium.js" data-cover></script>
|
||||
|
||||
<script type='text/javascript' src='../js/views/backbone_wrapper_view.js'></script>
|
||||
<script type='text/javascript' src='../js/views/whisper_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/debug_log_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/toast_view.js' data-cover></script>
|
||||
|
|
42
test/legacy_bridge.js
Normal file
42
test/legacy_bridge.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
|
||||
// Because we aren't hosting the styleguide in Electron, we can't rely on preload.js
|
||||
// to set things up for us. This gives us the minimum bar shims for everything it
|
||||
// provdes.
|
||||
//
|
||||
// Remember, the idea here is just to enable visual testing, no full functionality. Most
|
||||
// of thise can be very simple.
|
||||
|
||||
window.PROTO_ROOT = '/protos';
|
||||
window.nodeSetImmediate = () => {};
|
||||
|
||||
window.Signal = {};
|
||||
window.Signal.Backup = {};
|
||||
window.Signal.Crypto = {};
|
||||
window.Signal.Logs = {};
|
||||
window.Signal.Migrations = {};
|
||||
|
||||
window.Signal.React = window.Signal.React = {};
|
||||
|
||||
window.EmojiConvertor = function EmojiConvertor() {};
|
||||
window.EmojiConvertor.prototype.init_colons = () => {}
|
||||
window.EmojiConvertor.prototype.signalReplace = html => html;
|
||||
window.EmojiConvertor.prototype.replace_unified = string => string;
|
||||
window.EmojiConvertor.prototype.img_sets = {
|
||||
apple: {}
|
||||
};
|
||||
|
||||
window.i18n = () => '';
|
||||
|
||||
window.Signal.Migrations.V17 = {};
|
||||
window.Signal.OS = {};
|
||||
window.Signal.Types = {};
|
||||
window.Signal.Types.Attachment = {};
|
||||
window.Signal.Types.Errors = {};
|
||||
window.Signal.Types.Message = {
|
||||
initializeSchemaVersion: attributes => attributes,
|
||||
};
|
||||
window.Signal.Types.MIME = {};
|
||||
window.Signal.Types.Settings = {};
|
||||
window.Signal.Views = {};
|
||||
window.Signal.Views.Initialization = {};
|
||||
window.Signal.Workflow = {};
|
45
test/legacy_templates.js
Normal file
45
test/legacy_templates.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
|
||||
// Taken from background.html.
|
||||
// Templates are here solely to support the Backbone views rendered in the styleguide.
|
||||
|
||||
window.Whisper.View.Templates = {
|
||||
hasRetry: `
|
||||
{{ messageNotSent }}
|
||||
<span href='#' class='retry'>{{ resend }}</span>
|
||||
`,
|
||||
'some-failed': `
|
||||
{{ someFailed }}
|
||||
`,
|
||||
keychange: `
|
||||
<span class='content' dir='auto'><span class='shield icon'></span> {{ content }}</span>
|
||||
`,
|
||||
'verified-change': `
|
||||
<span class='content' dir='auto'><span class='{{ icon }} icon'></span> {{ content }}</span>
|
||||
`,
|
||||
message: `
|
||||
{{> avatar }}
|
||||
<div class='bubble {{ avatar.color }}'>
|
||||
<div class='sender' dir='auto'>
|
||||
{{ sender }}
|
||||
{{ #profileName }}
|
||||
<span class='profileName'>{{ profileName }} </span>
|
||||
{{ /profileName }}
|
||||
</div>
|
||||
<div class='attachments'></div>
|
||||
<p class='content' dir='auto'>
|
||||
{{ #message }}<span class='body'>{{ message }}</span>{{ /message }}
|
||||
</p>
|
||||
<div class='meta'>
|
||||
<span class='timestamp' data-timestamp={{ timestamp }}></span>
|
||||
<span class='status hide'></span>
|
||||
<span class='timer'></span>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
hourglass: `
|
||||
<span class='hourglass'><span class='sand'></span></span>
|
||||
`,
|
||||
expirationTimerUpdate: `
|
||||
<span class='content'><span class='icon clock'></span> {{ content }}</span>
|
||||
`
|
||||
};
|
Loading…
Reference in a new issue