Persist drafts
This commit is contained in:
parent
5ebd8bc690
commit
9d4f2afa5a
23 changed files with 1048 additions and 720 deletions
|
@ -1720,6 +1720,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"ConversationListItem--draft-prefix": {
|
||||||
|
"message": "Draft:",
|
||||||
|
"description":
|
||||||
|
"Prefix shown in italic in conversation view when a draft is saved"
|
||||||
|
},
|
||||||
"message--getNotificationText--stickers": {
|
"message--getNotificationText--stickers": {
|
||||||
"message": "Sticker message",
|
"message": "Sticker message",
|
||||||
"description":
|
"description":
|
||||||
|
@ -1906,5 +1912,20 @@
|
||||||
"message": "View Photo",
|
"message": "View Photo",
|
||||||
"description":
|
"description":
|
||||||
"Text shown on messages with with individual timers, before user has viewed it"
|
"Text shown on messages with with individual timers, before user has viewed it"
|
||||||
|
},
|
||||||
|
"Conversation--getDraftPreview--attachment": {
|
||||||
|
"message": "(attachment)",
|
||||||
|
"description":
|
||||||
|
"Text shown in left pane as preview for conversation with saved a saved draft message"
|
||||||
|
},
|
||||||
|
"Conversation--getDraftPreview--quote": {
|
||||||
|
"message": "(quote)",
|
||||||
|
"description":
|
||||||
|
"Text shown in left pane as preview for conversation with saved a saved draft message"
|
||||||
|
},
|
||||||
|
"Conversation--getDraftPreview--draft": {
|
||||||
|
"message": "(draft)",
|
||||||
|
"description":
|
||||||
|
"Text shown in left pane as preview for conversation with saved a saved draft message"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ let initialized = false;
|
||||||
const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
|
const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
|
||||||
const ERASE_STICKERS_KEY = 'erase-stickers';
|
const ERASE_STICKERS_KEY = 'erase-stickers';
|
||||||
const ERASE_TEMP_KEY = 'erase-temp';
|
const ERASE_TEMP_KEY = 'erase-temp';
|
||||||
|
const ERASE_DRAFTS_KEY = 'erase-drafts';
|
||||||
const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
|
const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
|
||||||
|
|
||||||
async function initialize({ configDir, cleanupOrphanedAttachments }) {
|
async function initialize({ configDir, cleanupOrphanedAttachments }) {
|
||||||
|
@ -24,6 +25,7 @@ async function initialize({ configDir, cleanupOrphanedAttachments }) {
|
||||||
const attachmentsDir = Attachments.getPath(configDir);
|
const attachmentsDir = Attachments.getPath(configDir);
|
||||||
const stickersDir = Attachments.getStickersPath(configDir);
|
const stickersDir = Attachments.getStickersPath(configDir);
|
||||||
const tempDir = Attachments.getTempPath(configDir);
|
const tempDir = Attachments.getTempPath(configDir);
|
||||||
|
const draftDir = Attachments.getDraftPath(configDir);
|
||||||
|
|
||||||
ipcMain.on(ERASE_TEMP_KEY, event => {
|
ipcMain.on(ERASE_TEMP_KEY, event => {
|
||||||
try {
|
try {
|
||||||
|
@ -58,6 +60,17 @@ async function initialize({ configDir, cleanupOrphanedAttachments }) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.on(ERASE_DRAFTS_KEY, event => {
|
||||||
|
try {
|
||||||
|
rimraf.sync(draftDir);
|
||||||
|
event.sender.send(`${ERASE_DRAFTS_KEY}-done`);
|
||||||
|
} catch (error) {
|
||||||
|
const errorForDisplay = error && error.stack ? error.stack : error;
|
||||||
|
console.log(`erase drafts error: ${errorForDisplay}`);
|
||||||
|
event.sender.send(`${ERASE_DRAFTS_KEY}-done`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.on(CLEANUP_ORPHANED_ATTACHMENTS_KEY, async event => {
|
ipcMain.on(CLEANUP_ORPHANED_ATTACHMENTS_KEY, async event => {
|
||||||
try {
|
try {
|
||||||
await cleanupOrphanedAttachments();
|
await cleanupOrphanedAttachments();
|
||||||
|
|
|
@ -10,6 +10,7 @@ const { map, isArrayBuffer, isString } = require('lodash');
|
||||||
const PATH = 'attachments.noindex';
|
const PATH = 'attachments.noindex';
|
||||||
const STICKER_PATH = 'stickers.noindex';
|
const STICKER_PATH = 'stickers.noindex';
|
||||||
const TEMP_PATH = 'temp';
|
const TEMP_PATH = 'temp';
|
||||||
|
const DRAFT_PATH = 'drafts.noindex';
|
||||||
|
|
||||||
exports.getAllAttachments = async userDataPath => {
|
exports.getAllAttachments = async userDataPath => {
|
||||||
const dir = exports.getPath(userDataPath);
|
const dir = exports.getPath(userDataPath);
|
||||||
|
@ -27,6 +28,14 @@ exports.getAllStickers = async userDataPath => {
|
||||||
return map(files, file => path.relative(dir, file));
|
return map(files, file => path.relative(dir, file));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.getAllDraftAttachments = async userDataPath => {
|
||||||
|
const dir = exports.getDraftPath(userDataPath);
|
||||||
|
const pattern = path.join(dir, '**', '*');
|
||||||
|
|
||||||
|
const files = await pify(glob)(pattern, { nodir: true });
|
||||||
|
return map(files, file => path.relative(dir, file));
|
||||||
|
};
|
||||||
|
|
||||||
// getPath :: AbsolutePath -> AbsolutePath
|
// getPath :: AbsolutePath -> AbsolutePath
|
||||||
exports.getPath = userDataPath => {
|
exports.getPath = userDataPath => {
|
||||||
if (!isString(userDataPath)) {
|
if (!isString(userDataPath)) {
|
||||||
|
@ -51,6 +60,14 @@ exports.getTempPath = userDataPath => {
|
||||||
return path.join(userDataPath, TEMP_PATH);
|
return path.join(userDataPath, TEMP_PATH);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// getDraftPath :: AbsolutePath -> AbsolutePath
|
||||||
|
exports.getDraftPath = userDataPath => {
|
||||||
|
if (!isString(userDataPath)) {
|
||||||
|
throw new TypeError("'userDataPath' must be a string");
|
||||||
|
}
|
||||||
|
return path.join(userDataPath, DRAFT_PATH);
|
||||||
|
};
|
||||||
|
|
||||||
// clearTempPath :: AbsolutePath -> AbsolutePath
|
// clearTempPath :: AbsolutePath -> AbsolutePath
|
||||||
exports.clearTempPath = userDataPath => {
|
exports.clearTempPath = userDataPath => {
|
||||||
const tempPath = exports.getTempPath(userDataPath);
|
const tempPath = exports.getTempPath(userDataPath);
|
||||||
|
@ -204,6 +221,20 @@ exports.deleteAllStickers = async ({ userDataPath, stickers }) => {
|
||||||
console.log(`deleteAllStickers: deleted ${stickers.length} files`);
|
console.log(`deleteAllStickers: deleted ${stickers.length} files`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.deleteAllDraftAttachments = async ({ userDataPath, stickers }) => {
|
||||||
|
const deleteFromDisk = exports.createDeleter(
|
||||||
|
exports.getDraftPath(userDataPath)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let index = 0, max = stickers.length; index < max; index += 1) {
|
||||||
|
const file = stickers[index];
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await deleteFromDisk(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`deleteAllDraftAttachments: deleted ${stickers.length} files`);
|
||||||
|
};
|
||||||
|
|
||||||
// createName :: Unit -> IO String
|
// createName :: Unit -> IO String
|
||||||
exports.createName = () => {
|
exports.createName = () => {
|
||||||
const buffer = crypto.randomBytes(32);
|
const buffer = crypto.randomBytes(32);
|
||||||
|
|
70
app/sql.js
70
app/sql.js
|
@ -140,6 +140,7 @@ module.exports = {
|
||||||
|
|
||||||
removeKnownAttachments,
|
removeKnownAttachments,
|
||||||
removeKnownStickers,
|
removeKnownStickers,
|
||||||
|
removeKnownDraftAttachments,
|
||||||
};
|
};
|
||||||
|
|
||||||
function generateUUID() {
|
function generateUUID() {
|
||||||
|
@ -2867,6 +2868,24 @@ function getExternalFilesForConversation(conversation) {
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getExternalDraftFilesForConversation(conversation) {
|
||||||
|
const draftAttachments = conversation.draftAttachments || [];
|
||||||
|
const files = [];
|
||||||
|
|
||||||
|
forEach(draftAttachments, attachment => {
|
||||||
|
const { path: file, screenshotPath } = attachment;
|
||||||
|
if (file) {
|
||||||
|
files.push(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (screenshotPath) {
|
||||||
|
files.push(screenshotPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
async function removeKnownAttachments(allAttachments) {
|
async function removeKnownAttachments(allAttachments) {
|
||||||
const lookup = fromPairs(map(allAttachments, file => [file, true]));
|
const lookup = fromPairs(map(allAttachments, file => [file, true]));
|
||||||
const chunkSize = 50;
|
const chunkSize = 50;
|
||||||
|
@ -2999,3 +3018,54 @@ async function removeKnownStickers(allStickers) {
|
||||||
|
|
||||||
return Object.keys(lookup);
|
return Object.keys(lookup);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function removeKnownDraftAttachments(allStickers) {
|
||||||
|
const lookup = fromPairs(map(allStickers, file => [file, true]));
|
||||||
|
const chunkSize = 50;
|
||||||
|
|
||||||
|
const total = await getConversationCount();
|
||||||
|
console.log(
|
||||||
|
`removeKnownDraftAttachments: About to iterate through ${total} conversations`
|
||||||
|
);
|
||||||
|
|
||||||
|
let complete = false;
|
||||||
|
let count = 0;
|
||||||
|
// Though conversations.id is a string, this ensures that, when coerced, this
|
||||||
|
// value is still a string but it's smaller than every other string.
|
||||||
|
let id = 0;
|
||||||
|
|
||||||
|
while (!complete) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const rows = await db.all(
|
||||||
|
`SELECT json FROM conversations
|
||||||
|
WHERE id > $id
|
||||||
|
ORDER BY id ASC
|
||||||
|
LIMIT $chunkSize;`,
|
||||||
|
{
|
||||||
|
$id: id,
|
||||||
|
$chunkSize: chunkSize,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const conversations = map(rows, row => jsonToObject(row.json));
|
||||||
|
forEach(conversations, conversation => {
|
||||||
|
const externalFiles = getExternalDraftFilesForConversation(conversation);
|
||||||
|
forEach(externalFiles, file => {
|
||||||
|
delete lookup[file];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const lastMessage = last(conversations);
|
||||||
|
if (lastMessage) {
|
||||||
|
({ id } = lastMessage);
|
||||||
|
}
|
||||||
|
complete = conversations.length < chunkSize;
|
||||||
|
count += conversations.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`removeKnownDraftAttachments: Done processing ${count} conversations`
|
||||||
|
);
|
||||||
|
|
||||||
|
return Object.keys(lookup);
|
||||||
|
}
|
||||||
|
|
|
@ -471,7 +471,6 @@
|
||||||
<script type='text/javascript' src='js/views/react_wrapper_view.js'></script>
|
<script type='text/javascript' src='js/views/react_wrapper_view.js'></script>
|
||||||
<script type='text/javascript' src='js/views/whisper_view.js'></script>
|
<script type='text/javascript' src='js/views/whisper_view.js'></script>
|
||||||
<script type='text/javascript' src='js/views/toast_view.js'></script>
|
<script type='text/javascript' src='js/views/toast_view.js'></script>
|
||||||
<script type='text/javascript' src='js/views/file_input_view.js'></script>
|
|
||||||
<script type='text/javascript' src='js/views/list_view.js'></script>
|
<script type='text/javascript' src='js/views/list_view.js'></script>
|
||||||
<script type='text/javascript' src='js/views/contact_list_view.js'></script>
|
<script type='text/javascript' src='js/views/contact_list_view.js'></script>
|
||||||
<script type='text/javascript' src='js/views/key_verification_view.js'></script>
|
<script type='text/javascript' src='js/views/key_verification_view.js'></script>
|
||||||
|
|
|
@ -150,6 +150,34 @@
|
||||||
return this.id === this.ourNumber;
|
return this.id === this.ourNumber;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
hasDraft() {
|
||||||
|
const draftAttachments = this.get('draftAttachments') || [];
|
||||||
|
return (
|
||||||
|
this.get('draft') ||
|
||||||
|
this.get('quotedMessageId') ||
|
||||||
|
draftAttachments.length > 0
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
getDraftPreview() {
|
||||||
|
const draft = this.get('draft');
|
||||||
|
if (draft) {
|
||||||
|
return draft;
|
||||||
|
}
|
||||||
|
|
||||||
|
const draftAttachments = this.get('draftAttachments') || [];
|
||||||
|
if (draftAttachments.length > 0) {
|
||||||
|
return i18n('Conversation--getDraftPreview--attachment');
|
||||||
|
}
|
||||||
|
|
||||||
|
const quotedMessageId = this.get('quotedMessageId');
|
||||||
|
if (quotedMessageId) {
|
||||||
|
return i18n('Conversation--getDraftPreview--quote');
|
||||||
|
}
|
||||||
|
|
||||||
|
return i18n('Conversation--getDraftPreview--draft');
|
||||||
|
},
|
||||||
|
|
||||||
bumpTyping() {
|
bumpTyping() {
|
||||||
// We don't send typing messages if the setting is disabled
|
// We don't send typing messages if the setting is disabled
|
||||||
if (!storage.get('typingIndicators')) {
|
if (!storage.get('typingIndicators')) {
|
||||||
|
@ -327,6 +355,13 @@
|
||||||
? ConversationController.getOrCreate(typingMostRecent.sender, 'private')
|
? ConversationController.getOrCreate(typingMostRecent.sender, 'private')
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const timestamp = this.get('timestamp');
|
||||||
|
const draftTimestamp = this.get('draftTimestamp');
|
||||||
|
const draftPreview = this.getDraftPreview();
|
||||||
|
const draftText = this.get('draft');
|
||||||
|
const shouldShowDraft =
|
||||||
|
this.hasDraft() && draftTimestamp && draftTimestamp >= timestamp;
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
|
||||||
|
@ -340,10 +375,14 @@
|
||||||
lastUpdated: this.get('timestamp'),
|
lastUpdated: this.get('timestamp'),
|
||||||
name: this.getName(),
|
name: this.getName(),
|
||||||
profileName: this.getProfileName(),
|
profileName: this.getProfileName(),
|
||||||
timestamp: this.get('timestamp'),
|
timestamp,
|
||||||
title: this.getTitle(),
|
title: this.getTitle(),
|
||||||
unreadCount: this.get('unreadCount') || 0,
|
unreadCount: this.get('unreadCount') || 0,
|
||||||
|
|
||||||
|
shouldShowDraft,
|
||||||
|
draftPreview,
|
||||||
|
draftText,
|
||||||
|
|
||||||
phoneNumber: format(this.id, {
|
phoneNumber: format(this.id, {
|
||||||
ourRegionCode: regionCode,
|
ourRegionCode: regionCode,
|
||||||
}),
|
}),
|
||||||
|
@ -970,6 +1009,8 @@
|
||||||
active_at: now,
|
active_at: now,
|
||||||
timestamp: now,
|
timestamp: now,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
|
draft: null,
|
||||||
|
draftTimestamp: null,
|
||||||
});
|
});
|
||||||
await window.Signal.Data.updateConversation(this.id, this.attributes, {
|
await window.Signal.Data.updateConversation(this.id, this.attributes, {
|
||||||
Conversation: Whisper.Conversation,
|
Conversation: Whisper.Conversation,
|
||||||
|
@ -1226,6 +1267,15 @@
|
||||||
);
|
);
|
||||||
|
|
||||||
const lastMessageModel = messages.at(0);
|
const lastMessageModel = messages.at(0);
|
||||||
|
if (
|
||||||
|
this.hasDraft() &&
|
||||||
|
this.get('draftTimestamp') &&
|
||||||
|
(!lastMessageModel ||
|
||||||
|
lastMessageModel.get('sent_at') < this.get('draftTimestamp'))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const lastMessageJSON = lastMessageModel
|
const lastMessageJSON = lastMessageModel
|
||||||
? lastMessageModel.toJSON()
|
? lastMessageModel.toJSON()
|
||||||
: null;
|
: null;
|
||||||
|
|
|
@ -9,7 +9,6 @@ const {
|
||||||
isFunction,
|
isFunction,
|
||||||
isObject,
|
isObject,
|
||||||
map,
|
map,
|
||||||
merge,
|
|
||||||
set,
|
set,
|
||||||
} = require('lodash');
|
} = require('lodash');
|
||||||
|
|
||||||
|
@ -29,6 +28,7 @@ const ERASE_SQL_KEY = 'erase-sql-key';
|
||||||
const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
|
const ERASE_ATTACHMENTS_KEY = 'erase-attachments';
|
||||||
const ERASE_STICKERS_KEY = 'erase-stickers';
|
const ERASE_STICKERS_KEY = 'erase-stickers';
|
||||||
const ERASE_TEMP_KEY = 'erase-temp';
|
const ERASE_TEMP_KEY = 'erase-temp';
|
||||||
|
const ERASE_DRAFTS_KEY = 'erase-drafts';
|
||||||
const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
|
const CLEANUP_ORPHANED_ATTACHMENTS_KEY = 'cleanup-orphaned-attachments';
|
||||||
|
|
||||||
const _jobs = Object.create(null);
|
const _jobs = Object.create(null);
|
||||||
|
@ -598,7 +598,10 @@ async function updateConversation(id, data, { Conversation }) {
|
||||||
throw new Error(`Conversation ${id} does not exist!`);
|
throw new Error(`Conversation ${id} does not exist!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const merged = merge({}, existing.attributes, data);
|
const merged = {
|
||||||
|
...existing.attributes,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
await channels.updateConversation(merged);
|
await channels.updateConversation(merged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1007,6 +1010,7 @@ async function removeOtherData() {
|
||||||
callChannel(ERASE_ATTACHMENTS_KEY),
|
callChannel(ERASE_ATTACHMENTS_KEY),
|
||||||
callChannel(ERASE_STICKERS_KEY),
|
callChannel(ERASE_STICKERS_KEY),
|
||||||
callChannel(ERASE_TEMP_KEY),
|
callChannel(ERASE_TEMP_KEY),
|
||||||
|
callChannel(ERASE_DRAFTS_KEY),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -103,20 +103,21 @@ function initializeMigrations({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const {
|
const {
|
||||||
|
createAbsolutePathGetter,
|
||||||
|
createReader,
|
||||||
|
createWriterForExisting,
|
||||||
|
createWriterForNew,
|
||||||
|
getDraftPath,
|
||||||
getPath,
|
getPath,
|
||||||
getStickersPath,
|
getStickersPath,
|
||||||
getTempPath,
|
getTempPath,
|
||||||
createReader,
|
|
||||||
createAbsolutePathGetter,
|
|
||||||
createWriterForNew,
|
|
||||||
createWriterForExisting,
|
|
||||||
} = Attachments;
|
} = Attachments;
|
||||||
const {
|
const {
|
||||||
makeObjectUrl,
|
|
||||||
revokeObjectUrl,
|
|
||||||
getImageDimensions,
|
getImageDimensions,
|
||||||
makeImageThumbnail,
|
makeImageThumbnail,
|
||||||
|
makeObjectUrl,
|
||||||
makeVideoScreenshot,
|
makeVideoScreenshot,
|
||||||
|
revokeObjectUrl,
|
||||||
} = VisualType;
|
} = VisualType;
|
||||||
|
|
||||||
const attachmentsPath = getPath(userDataPath);
|
const attachmentsPath = getPath(userDataPath);
|
||||||
|
@ -147,11 +148,18 @@ function initializeMigrations({
|
||||||
tempPath
|
tempPath
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const draftPath = getDraftPath(userDataPath);
|
||||||
|
const getAbsoluteDraftPath = createAbsolutePathGetter(draftPath);
|
||||||
|
const writeNewDraftData = createWriterForNew(draftPath);
|
||||||
|
const deleteDraftFile = Attachments.createDeleter(draftPath);
|
||||||
|
const readDraftData = createReader(draftPath);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
attachmentsPath,
|
attachmentsPath,
|
||||||
copyIntoAttachmentsDirectory,
|
copyIntoAttachmentsDirectory,
|
||||||
copyIntoTempDirectory,
|
copyIntoTempDirectory,
|
||||||
deleteAttachmentData: deleteOnDisk,
|
deleteAttachmentData: deleteOnDisk,
|
||||||
|
deleteDraftFile,
|
||||||
deleteExternalMessageFiles: MessageType.deleteAllExternalFiles({
|
deleteExternalMessageFiles: MessageType.deleteAllExternalFiles({
|
||||||
deleteAttachmentData: Type.deleteData(deleteOnDisk),
|
deleteAttachmentData: Type.deleteData(deleteOnDisk),
|
||||||
deleteOnDisk,
|
deleteOnDisk,
|
||||||
|
@ -159,6 +167,7 @@ function initializeMigrations({
|
||||||
deleteSticker,
|
deleteSticker,
|
||||||
deleteTempFile,
|
deleteTempFile,
|
||||||
getAbsoluteAttachmentPath,
|
getAbsoluteAttachmentPath,
|
||||||
|
getAbsoluteDraftPath,
|
||||||
getAbsoluteStickerPath,
|
getAbsoluteStickerPath,
|
||||||
getAbsoluteTempPath,
|
getAbsoluteTempPath,
|
||||||
getPlaceholderMigrations,
|
getPlaceholderMigrations,
|
||||||
|
@ -169,6 +178,7 @@ function initializeMigrations({
|
||||||
loadQuoteData,
|
loadQuoteData,
|
||||||
loadStickerData,
|
loadStickerData,
|
||||||
readAttachmentData,
|
readAttachmentData,
|
||||||
|
readDraftData,
|
||||||
readStickerData,
|
readStickerData,
|
||||||
readTempData,
|
readTempData,
|
||||||
run,
|
run,
|
||||||
|
@ -218,6 +228,7 @@ function initializeMigrations({
|
||||||
logger,
|
logger,
|
||||||
}),
|
}),
|
||||||
writeNewAttachmentData: createWriterForNew(attachmentsPath),
|
writeNewAttachmentData: createWriterForNew(attachmentsPath),
|
||||||
|
writeNewDraftData,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,575 +0,0 @@
|
||||||
/* global textsecure: false */
|
|
||||||
/* global Whisper: false */
|
|
||||||
/* global i18n: false */
|
|
||||||
/* global loadImage: false */
|
|
||||||
/* global Backbone: false */
|
|
||||||
/* global _: false */
|
|
||||||
/* global Signal: false */
|
|
||||||
|
|
||||||
// eslint-disable-next-line func-names
|
|
||||||
(function() {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
window.Whisper = window.Whisper || {};
|
|
||||||
|
|
||||||
const { MIME, VisualAttachment } = window.Signal.Types;
|
|
||||||
|
|
||||||
Whisper.FileSizeToast = Whisper.ToastView.extend({
|
|
||||||
templateName: 'file-size-modal',
|
|
||||||
render_attributes() {
|
|
||||||
return {
|
|
||||||
'file-size-warning': i18n('fileSizeWarning'),
|
|
||||||
limit: this.model.limit,
|
|
||||||
units: this.model.units,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Whisper.UnableToLoadToast = Whisper.ToastView.extend({
|
|
||||||
render_attributes() {
|
|
||||||
return { toastMessage: i18n('unableToLoadAttachment') };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
Whisper.DangerousFileTypeToast = Whisper.ToastView.extend({
|
|
||||||
template: i18n('dangerousFileType'),
|
|
||||||
});
|
|
||||||
Whisper.OneNonImageAtATimeToast = Whisper.ToastView.extend({
|
|
||||||
template: i18n('oneNonImageAtATimeToast'),
|
|
||||||
});
|
|
||||||
Whisper.CannotMixImageAndNonImageAttachmentsToast = Whisper.ToastView.extend({
|
|
||||||
template: i18n('cannotMixImageAdnNonImageAttachments'),
|
|
||||||
});
|
|
||||||
Whisper.MaxAttachmentsToast = Whisper.ToastView.extend({
|
|
||||||
template: i18n('maximumAttachments'),
|
|
||||||
});
|
|
||||||
|
|
||||||
Whisper.FileInputView = Backbone.View.extend({
|
|
||||||
tagName: 'span',
|
|
||||||
className: 'file-input',
|
|
||||||
initialize() {
|
|
||||||
this.attachments = [];
|
|
||||||
|
|
||||||
this.attachmentListView = new Whisper.ReactWrapperView({
|
|
||||||
el: this.el,
|
|
||||||
Component: window.Signal.Components.AttachmentList,
|
|
||||||
props: this.getPropsForAttachmentList(),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
remove() {
|
|
||||||
if (this.attachmentListView) {
|
|
||||||
this.attachmentListView.remove();
|
|
||||||
}
|
|
||||||
if (this.captionEditorView) {
|
|
||||||
this.captionEditorView.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
Backbone.View.prototype.remove.call(this);
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
this.attachmentListView.update(this.getPropsForAttachmentList());
|
|
||||||
this.trigger('staged-attachments-changed');
|
|
||||||
},
|
|
||||||
|
|
||||||
getPropsForAttachmentList() {
|
|
||||||
const { attachments } = this;
|
|
||||||
|
|
||||||
// We never want to display voice notes in our attachment list
|
|
||||||
if (_.any(attachments, attachment => Boolean(attachment.isVoiceNote))) {
|
|
||||||
return {
|
|
||||||
attachments: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
attachments,
|
|
||||||
onAddAttachment: this.onAddAttachment.bind(this),
|
|
||||||
onClickAttachment: this.onClickAttachment.bind(this),
|
|
||||||
onCloseAttachment: this.onCloseAttachment.bind(this),
|
|
||||||
onClose: this.onClose.bind(this),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
onClickAttachment(attachment) {
|
|
||||||
const getProps = () => ({
|
|
||||||
url: attachment.videoUrl || attachment.url,
|
|
||||||
caption: attachment.caption,
|
|
||||||
attachment,
|
|
||||||
onSave,
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSave = caption => {
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
attachment.caption = caption;
|
|
||||||
this.captionEditorView.remove();
|
|
||||||
Signal.Backbone.Views.Lightbox.hide();
|
|
||||||
this.render();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.captionEditorView = new Whisper.ReactWrapperView({
|
|
||||||
className: 'attachment-list-wrapper',
|
|
||||||
Component: window.Signal.Components.CaptionEditor,
|
|
||||||
props: getProps(),
|
|
||||||
onClose: () => Signal.Backbone.Views.Lightbox.hide(),
|
|
||||||
});
|
|
||||||
Signal.Backbone.Views.Lightbox.show(this.captionEditorView.el);
|
|
||||||
},
|
|
||||||
|
|
||||||
onCloseAttachment(attachment) {
|
|
||||||
this.attachments = _.without(this.attachments, attachment);
|
|
||||||
this.render();
|
|
||||||
},
|
|
||||||
|
|
||||||
onAddAttachment() {
|
|
||||||
this.trigger('choose-attachment');
|
|
||||||
},
|
|
||||||
|
|
||||||
onClose() {
|
|
||||||
this.attachments = [];
|
|
||||||
this.render();
|
|
||||||
},
|
|
||||||
|
|
||||||
// These event handlers are called by ConversationView, which listens for these events
|
|
||||||
|
|
||||||
onDragOver(e) {
|
|
||||||
if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
this.$el.addClass('dropoff');
|
|
||||||
},
|
|
||||||
|
|
||||||
onDragLeave(e) {
|
|
||||||
if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
this.$el.removeClass('dropoff');
|
|
||||||
},
|
|
||||||
|
|
||||||
async onDrop(e) {
|
|
||||||
if (e.originalEvent.dataTransfer.types[0] !== 'Files') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const { files } = e.originalEvent.dataTransfer;
|
|
||||||
for (let i = 0, max = files.length; i < max; i += 1) {
|
|
||||||
const file = files[i];
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await this.maybeAddAttachment(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$el.removeClass('dropoff');
|
|
||||||
},
|
|
||||||
|
|
||||||
onPaste(e) {
|
|
||||||
const { items } = e.originalEvent.clipboardData;
|
|
||||||
let imgBlob = null;
|
|
||||||
for (let i = 0; i < items.length; i += 1) {
|
|
||||||
if (items[i].type.split('/')[0] === 'image') {
|
|
||||||
imgBlob = items[i].getAsFile();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (imgBlob !== null) {
|
|
||||||
const file = imgBlob;
|
|
||||||
this.maybeAddAttachment(file);
|
|
||||||
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Public interface
|
|
||||||
|
|
||||||
hasFiles() {
|
|
||||||
return this.attachments.length > 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
async getFiles() {
|
|
||||||
const files = await Promise.all(
|
|
||||||
this.attachments.map(attachment => this.getFile(attachment))
|
|
||||||
);
|
|
||||||
this.clearAttachments();
|
|
||||||
return files;
|
|
||||||
},
|
|
||||||
|
|
||||||
clearAttachments() {
|
|
||||||
this.attachments.forEach(attachment => {
|
|
||||||
if (attachment.url) {
|
|
||||||
URL.revokeObjectURL(attachment.url);
|
|
||||||
}
|
|
||||||
if (attachment.videoUrl) {
|
|
||||||
URL.revokeObjectURL(attachment.videoUrl);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.attachments = [];
|
|
||||||
this.render();
|
|
||||||
this.$el.trigger('force-resize');
|
|
||||||
},
|
|
||||||
|
|
||||||
// Show errors
|
|
||||||
|
|
||||||
showLoadFailure() {
|
|
||||||
const toast = new Whisper.UnableToLoadToast();
|
|
||||||
toast.$el.insertAfter(this.$el);
|
|
||||||
toast.render();
|
|
||||||
},
|
|
||||||
|
|
||||||
showDangerousError() {
|
|
||||||
const toast = new Whisper.DangerousFileTypeToast();
|
|
||||||
toast.$el.insertAfter(this.$el);
|
|
||||||
toast.render();
|
|
||||||
},
|
|
||||||
|
|
||||||
showFileSizeError({ limit, units, u }) {
|
|
||||||
const toast = new Whisper.FileSizeToast({
|
|
||||||
model: { limit, units: units[u] },
|
|
||||||
});
|
|
||||||
toast.$el.insertAfter(this.$el);
|
|
||||||
toast.render();
|
|
||||||
},
|
|
||||||
|
|
||||||
showCannotMixError() {
|
|
||||||
const toast = new Whisper.CannotMixImageAndNonImageAttachmentsToast();
|
|
||||||
toast.$el.insertAfter(this.$el);
|
|
||||||
toast.render();
|
|
||||||
},
|
|
||||||
|
|
||||||
showMultipleNonImageError() {
|
|
||||||
const toast = new Whisper.OneNonImageAtATimeToast();
|
|
||||||
toast.$el.insertAfter(this.$el);
|
|
||||||
toast.render();
|
|
||||||
},
|
|
||||||
|
|
||||||
showMaximumAttachmentsError() {
|
|
||||||
const toast = new Whisper.MaxAttachmentsToast();
|
|
||||||
toast.$el.insertAfter(this.$el);
|
|
||||||
toast.render();
|
|
||||||
},
|
|
||||||
|
|
||||||
// Housekeeping
|
|
||||||
|
|
||||||
addAttachment(attachment) {
|
|
||||||
if (attachment.isVoiceNote && this.attachments.length > 0) {
|
|
||||||
throw new Error('A voice note cannot be sent with other attachments');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.attachments.push(attachment);
|
|
||||||
this.render();
|
|
||||||
},
|
|
||||||
|
|
||||||
async maybeAddAttachment(file) {
|
|
||||||
if (!file) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileName = file.name;
|
|
||||||
const contentType = file.type;
|
|
||||||
|
|
||||||
if (window.Signal.Util.isFileDangerous(fileName)) {
|
|
||||||
this.showDangerousError();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.attachments.length >= 32) {
|
|
||||||
this.showMaximumAttachmentsError();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const haveNonImage = _.any(
|
|
||||||
this.attachments,
|
|
||||||
attachment => !MIME.isImage(attachment.contentType)
|
|
||||||
);
|
|
||||||
// You can't add another attachment if you already have a non-image staged
|
|
||||||
if (haveNonImage) {
|
|
||||||
this.showMultipleNonImageError();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// You can't add a non-image attachment if you already have attachments staged
|
|
||||||
if (!MIME.isImage(contentType) && this.attachments.length > 0) {
|
|
||||||
this.showCannotMixError();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderVideoPreview = async () => {
|
|
||||||
const objectUrl = URL.createObjectURL(file);
|
|
||||||
try {
|
|
||||||
const type = 'image/png';
|
|
||||||
const thumbnail = await VisualAttachment.makeVideoScreenshot({
|
|
||||||
objectUrl,
|
|
||||||
contentType: type,
|
|
||||||
logger: window.log,
|
|
||||||
});
|
|
||||||
const data = await VisualAttachment.blobToArrayBuffer(thumbnail);
|
|
||||||
const url = Signal.Util.arrayBufferToObjectURL({
|
|
||||||
data,
|
|
||||||
type,
|
|
||||||
});
|
|
||||||
this.addAttachment({
|
|
||||||
file,
|
|
||||||
size: file.size,
|
|
||||||
fileName,
|
|
||||||
contentType,
|
|
||||||
videoUrl: objectUrl,
|
|
||||||
url,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
URL.revokeObjectURL(objectUrl);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderImagePreview = async () => {
|
|
||||||
if (!MIME.isJPEG(contentType)) {
|
|
||||||
const url = URL.createObjectURL(file);
|
|
||||||
if (!url) {
|
|
||||||
throw new Error('Failed to create object url for image!');
|
|
||||||
}
|
|
||||||
this.addAttachment({
|
|
||||||
file,
|
|
||||||
size: file.size,
|
|
||||||
fileName,
|
|
||||||
contentType,
|
|
||||||
url,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = await window.autoOrientImage(file);
|
|
||||||
this.addAttachment({
|
|
||||||
file,
|
|
||||||
size: file.size,
|
|
||||||
fileName,
|
|
||||||
contentType,
|
|
||||||
url,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const blob = await this.autoScale({
|
|
||||||
contentType,
|
|
||||||
file,
|
|
||||||
});
|
|
||||||
let limitKb = 1000000;
|
|
||||||
const blobType =
|
|
||||||
file.type === 'image/gif' ? 'gif' : contentType.split('/')[0];
|
|
||||||
|
|
||||||
switch (blobType) {
|
|
||||||
case 'image':
|
|
||||||
limitKb = 6000;
|
|
||||||
break;
|
|
||||||
case 'gif':
|
|
||||||
limitKb = 25000;
|
|
||||||
break;
|
|
||||||
case 'audio':
|
|
||||||
limitKb = 100000;
|
|
||||||
break;
|
|
||||||
case 'video':
|
|
||||||
limitKb = 100000;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
limitKb = 100000;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if ((blob.file.size / 1024).toFixed(4) >= limitKb) {
|
|
||||||
const units = ['kB', 'MB', 'GB'];
|
|
||||||
let u = -1;
|
|
||||||
let limit = limitKb * 1000;
|
|
||||||
do {
|
|
||||||
limit /= 1000;
|
|
||||||
u += 1;
|
|
||||||
} while (limit >= 1000 && u < units.length - 1);
|
|
||||||
this.showFileSizeError({ limit, units, u });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
window.log.error(
|
|
||||||
'Error ensuring that image is properly sized:',
|
|
||||||
error && error.stack ? error.stack : error
|
|
||||||
);
|
|
||||||
|
|
||||||
this.showLoadFailure();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (Signal.Util.GoogleChrome.isImageTypeSupported(contentType)) {
|
|
||||||
await renderImagePreview();
|
|
||||||
} else if (Signal.Util.GoogleChrome.isVideoTypeSupported(contentType)) {
|
|
||||||
await renderVideoPreview();
|
|
||||||
} else {
|
|
||||||
this.addAttachment({
|
|
||||||
file,
|
|
||||||
size: file.size,
|
|
||||||
contentType,
|
|
||||||
fileName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
window.log.error(
|
|
||||||
`Was unable to generate thumbnail for file type ${contentType}`,
|
|
||||||
e && e.stack ? e.stack : e
|
|
||||||
);
|
|
||||||
this.addAttachment({
|
|
||||||
file,
|
|
||||||
size: file.size,
|
|
||||||
contentType,
|
|
||||||
fileName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
autoScale(attachment) {
|
|
||||||
const { contentType, file } = attachment;
|
|
||||||
if (
|
|
||||||
contentType.split('/')[0] !== 'image' ||
|
|
||||||
contentType === 'image/tiff'
|
|
||||||
) {
|
|
||||||
// nothing to do
|
|
||||||
return Promise.resolve(attachment);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const url = URL.createObjectURL(file);
|
|
||||||
const img = document.createElement('img');
|
|
||||||
img.onerror = reject;
|
|
||||||
img.onload = () => {
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
|
|
||||||
const maxSize = 6000 * 1024;
|
|
||||||
const maxHeight = 4096;
|
|
||||||
const maxWidth = 4096;
|
|
||||||
if (
|
|
||||||
img.naturalWidth <= maxWidth &&
|
|
||||||
img.naturalHeight <= maxHeight &&
|
|
||||||
file.size <= maxSize
|
|
||||||
) {
|
|
||||||
resolve(attachment);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const gifMaxSize = 25000 * 1024;
|
|
||||||
if (file.type === 'image/gif' && file.size <= gifMaxSize) {
|
|
||||||
resolve(attachment);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.type === 'image/gif') {
|
|
||||||
reject(new Error('GIF is too large'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetContentType = 'image/jpeg';
|
|
||||||
const canvas = loadImage.scale(img, {
|
|
||||||
canvas: true,
|
|
||||||
maxWidth,
|
|
||||||
maxHeight,
|
|
||||||
});
|
|
||||||
|
|
||||||
let quality = 0.95;
|
|
||||||
let i = 4;
|
|
||||||
let blob;
|
|
||||||
do {
|
|
||||||
i -= 1;
|
|
||||||
blob = window.dataURLToBlobSync(
|
|
||||||
canvas.toDataURL(targetContentType, quality)
|
|
||||||
);
|
|
||||||
quality = quality * maxSize / blob.size;
|
|
||||||
// NOTE: During testing with a large image, we observed the
|
|
||||||
// `quality` value being > 1. Should we clamp it to [0.5, 1.0]?
|
|
||||||
// See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#Syntax
|
|
||||||
if (quality < 0.5) {
|
|
||||||
quality = 0.5;
|
|
||||||
}
|
|
||||||
} while (i > 0 && blob.size > maxSize);
|
|
||||||
|
|
||||||
resolve({
|
|
||||||
...attachment,
|
|
||||||
fileName: this.fixExtension(attachment.fileName, targetContentType),
|
|
||||||
contentType: targetContentType,
|
|
||||||
file: blob,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
img.src = url;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
getFileName(fileName) {
|
|
||||||
if (!fileName) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fileName.includes('.')) {
|
|
||||||
return fileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fileName
|
|
||||||
.split('.')
|
|
||||||
.slice(0, -1)
|
|
||||||
.join('.');
|
|
||||||
},
|
|
||||||
|
|
||||||
getType(contentType) {
|
|
||||||
if (!contentType) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!contentType.includes('/')) {
|
|
||||||
return contentType;
|
|
||||||
}
|
|
||||||
|
|
||||||
return contentType.split('/')[1];
|
|
||||||
},
|
|
||||||
|
|
||||||
fixExtension(fileName, contentType) {
|
|
||||||
const extension = this.getType(contentType);
|
|
||||||
const name = this.getFileName(fileName);
|
|
||||||
return `${name}.${extension}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
async getFile(attachment) {
|
|
||||||
if (!attachment) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
const attachmentFlags = attachment.isVoiceNote
|
|
||||||
? textsecure.protobuf.AttachmentPointer.Flags.VOICE_MESSAGE
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const scaled = await this.autoScale(attachment);
|
|
||||||
const fileRead = await this.readFile(scaled);
|
|
||||||
return {
|
|
||||||
...fileRead,
|
|
||||||
url: undefined,
|
|
||||||
videoUrl: undefined,
|
|
||||||
flags: attachmentFlags || null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
readFile(attachment) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const FR = new FileReader();
|
|
||||||
FR.onload = e => {
|
|
||||||
const data = e.target.result;
|
|
||||||
resolve({
|
|
||||||
...attachment,
|
|
||||||
data,
|
|
||||||
size: data.byteLength,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
FR.onerror = reject;
|
|
||||||
FR.onabort = reject;
|
|
||||||
FR.readAsArrayBuffer(attachment.file);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})();
|
|
11
main.js
11
main.js
|
@ -724,6 +724,17 @@ app.on('ready', async () => {
|
||||||
userDataPath,
|
userDataPath,
|
||||||
stickers: orphanedStickers,
|
stickers: orphanedStickers,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const allDraftAttachments = await attachments.getAllDraftAttachments(
|
||||||
|
userDataPath
|
||||||
|
);
|
||||||
|
const orphanedDraftAttachments = await sql.removeKnownDraftAttachments(
|
||||||
|
allDraftAttachments
|
||||||
|
);
|
||||||
|
await attachments.deleteAllDraftAttachments({
|
||||||
|
userDataPath,
|
||||||
|
stickers: orphanedDraftAttachments,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
14
preload.js
14
preload.js
|
@ -5,8 +5,17 @@ const semver = require('semver');
|
||||||
|
|
||||||
const { deferredToPromise } = require('./js/modules/deferred_to_promise');
|
const { deferredToPromise } = require('./js/modules/deferred_to_promise');
|
||||||
|
|
||||||
const { app } = electron.remote;
|
const { remote } = electron;
|
||||||
const { systemPreferences } = electron.remote.require('electron');
|
const { app } = remote;
|
||||||
|
const { systemPreferences } = remote.require('electron');
|
||||||
|
|
||||||
|
const browserWindow = remote.getCurrentWindow();
|
||||||
|
let focusHandlers = [];
|
||||||
|
browserWindow.on('focus', () => focusHandlers.forEach(handler => handler()));
|
||||||
|
window.registerForFocus = handler => focusHandlers.push(handler);
|
||||||
|
window.unregisterForFocus = handler => {
|
||||||
|
focusHandlers = focusHandlers.filter(item => item !== handler);
|
||||||
|
};
|
||||||
|
|
||||||
// Waiting for clients to implement changes on receive side
|
// Waiting for clients to implement changes on receive side
|
||||||
window.ENABLE_STICKER_SEND = true;
|
window.ENABLE_STICKER_SEND = true;
|
||||||
|
@ -308,6 +317,7 @@ const userDataPath = app.getPath('userData');
|
||||||
window.baseAttachmentsPath = Attachments.getPath(userDataPath);
|
window.baseAttachmentsPath = Attachments.getPath(userDataPath);
|
||||||
window.baseStickersPath = Attachments.getStickersPath(userDataPath);
|
window.baseStickersPath = Attachments.getStickersPath(userDataPath);
|
||||||
window.baseTempPath = Attachments.getTempPath(userDataPath);
|
window.baseTempPath = Attachments.getTempPath(userDataPath);
|
||||||
|
window.baseDraftPath = Attachments.getDraftPath(userDataPath);
|
||||||
window.Signal = Signal.setup({
|
window.Signal = Signal.setup({
|
||||||
Attachments,
|
Attachments,
|
||||||
userDataPath,
|
userDataPath,
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
|
||||||
&:after {
|
&::after {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 0;
|
font-size: 0;
|
||||||
|
@ -1689,7 +1689,7 @@
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
padding-bottom: 20px;
|
padding-bottom: 20px;
|
||||||
|
|
||||||
&:after {
|
&::after {
|
||||||
content: '.';
|
content: '.';
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -2146,6 +2146,11 @@
|
||||||
color: $color-gray-90;
|
color: $color-gray-90;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-conversation-list-item__message__draft-prefix {
|
||||||
|
font-style: italic;
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
.module-conversation-list-item__message__status-icon {
|
.module-conversation-list-item__message__status-icon {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
@ -2387,7 +2392,7 @@
|
||||||
color: $color-gray-90;
|
color: $color-gray-90;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|
||||||
&::placeholder {
|
&:placeholder {
|
||||||
color: $color-gray-45;
|
color: $color-gray-45;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2898,7 +2903,7 @@
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
padding-right: 65px;
|
padding-right: 65px;
|
||||||
|
|
||||||
&::placeholder {
|
&:placeholder {
|
||||||
color: $color-white-07;
|
color: $color-white-07;
|
||||||
}
|
}
|
||||||
&:focus {
|
&:focus {
|
||||||
|
@ -3861,7 +3866,7 @@
|
||||||
background: none;
|
background: none;
|
||||||
border: 0;
|
border: 0;
|
||||||
&--menu {
|
&--menu {
|
||||||
&:after {
|
&::after {
|
||||||
content: '';
|
content: '';
|
||||||
display: block;
|
display: block;
|
||||||
min-width: 24px;
|
min-width: 24px;
|
||||||
|
@ -4123,7 +4128,7 @@
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:after {
|
&::after {
|
||||||
display: block;
|
display: block;
|
||||||
content: '';
|
content: '';
|
||||||
width: 24px;
|
width: 24px;
|
||||||
|
@ -4447,7 +4452,7 @@
|
||||||
border-color: $color-signal-blue;
|
border-color: $color-signal-blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::placeholder {
|
&:placeholder {
|
||||||
color: $color-gray-45;
|
color: $color-gray-45;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4461,7 +4466,7 @@
|
||||||
border-color: $color-signal-blue;
|
border-color: $color-signal-blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::placeholder {
|
&:placeholder {
|
||||||
color: $color-gray-45;
|
color: $color-gray-45;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4642,7 +4647,7 @@
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:after {
|
&::after {
|
||||||
display: block;
|
display: block;
|
||||||
content: '';
|
content: '';
|
||||||
width: 24px;
|
width: 24px;
|
||||||
|
@ -4987,7 +4992,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
&:after {
|
&::after {
|
||||||
display: block;
|
display: block;
|
||||||
content: '';
|
content: '';
|
||||||
width: 24px;
|
width: 24px;
|
||||||
|
|
|
@ -484,7 +484,6 @@
|
||||||
<script type='text/javascript' src='../js/views/whisper_view.js' data-cover></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/debug_log_view.js' data-cover></script>
|
||||||
<script type='text/javascript' src='../js/views/toast_view.js' data-cover></script>
|
<script type='text/javascript' src='../js/views/toast_view.js' data-cover></script>
|
||||||
<script type='text/javascript' src='../js/views/file_input_view.js' data-cover></script>
|
|
||||||
<script type='text/javascript' src='../js/views/list_view.js' data-cover></script>
|
<script type='text/javascript' src='../js/views/list_view.js' data-cover></script>
|
||||||
<script type='text/javascript' src='../js/views/contact_list_view.js' data-cover></script>
|
<script type='text/javascript' src='../js/views/contact_list_view.js' data-cover></script>
|
||||||
<script type='text/javascript' src='../js/views/key_verification_view.js' data-cover></script>
|
<script type='text/javascript' src='../js/views/key_verification_view.js' data-cover></script>
|
||||||
|
|
|
@ -33,11 +33,12 @@ export type OwnProps = {
|
||||||
readonly micCellEl?: HTMLElement;
|
readonly micCellEl?: HTMLElement;
|
||||||
readonly attCellEl?: HTMLElement;
|
readonly attCellEl?: HTMLElement;
|
||||||
readonly attachmentListEl?: HTMLElement;
|
readonly attachmentListEl?: HTMLElement;
|
||||||
|
onChooseAttachment(): unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Props = Pick<
|
export type Props = Pick<
|
||||||
CompositionInputProps,
|
CompositionInputProps,
|
||||||
'onSubmit' | 'onEditorSizeChange' | 'onEditorStateChange'
|
'onSubmit' | 'onEditorSizeChange' | 'onEditorStateChange' | 'startingText'
|
||||||
> &
|
> &
|
||||||
Pick<
|
Pick<
|
||||||
EmojiButtonProps,
|
EmojiButtonProps,
|
||||||
|
@ -69,12 +70,13 @@ export const CompositionArea = ({
|
||||||
i18n,
|
i18n,
|
||||||
attachmentListEl,
|
attachmentListEl,
|
||||||
micCellEl,
|
micCellEl,
|
||||||
attCellEl,
|
onChooseAttachment,
|
||||||
// CompositionInput
|
// CompositionInput
|
||||||
onSubmit,
|
onSubmit,
|
||||||
compositionApi,
|
compositionApi,
|
||||||
onEditorSizeChange,
|
onEditorSizeChange,
|
||||||
onEditorStateChange,
|
onEditorStateChange,
|
||||||
|
startingText,
|
||||||
// EmojiButton
|
// EmojiButton
|
||||||
onPickEmoji,
|
onPickEmoji,
|
||||||
onSetSkinTone,
|
onSetSkinTone,
|
||||||
|
@ -94,7 +96,7 @@ export const CompositionArea = ({
|
||||||
clearShowPickerHint,
|
clearShowPickerHint,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [disabled, setDisabled] = React.useState(false);
|
const [disabled, setDisabled] = React.useState(false);
|
||||||
const [showMic, setShowMic] = React.useState(true);
|
const [showMic, setShowMic] = React.useState(!startingText);
|
||||||
const [micActive, setMicActive] = React.useState(false);
|
const [micActive, setMicActive] = React.useState(false);
|
||||||
const [dirty, setDirty] = React.useState(false);
|
const [dirty, setDirty] = React.useState(false);
|
||||||
const [large, setLarge] = React.useState(false);
|
const [large, setLarge] = React.useState(false);
|
||||||
|
@ -179,23 +181,17 @@ export const CompositionArea = ({
|
||||||
// The following is a work-around to allow react to lay-out backbone-managed
|
// The following is a work-around to allow react to lay-out backbone-managed
|
||||||
// dom nodes until those functions are in React
|
// dom nodes until those functions are in React
|
||||||
const micCellRef = React.useRef<HTMLDivElement>(null);
|
const micCellRef = React.useRef<HTMLDivElement>(null);
|
||||||
const attCellRef = React.useRef<HTMLDivElement>(null);
|
|
||||||
React.useLayoutEffect(
|
React.useLayoutEffect(
|
||||||
() => {
|
() => {
|
||||||
const { current: micCellContainer } = micCellRef;
|
const { current: micCellContainer } = micCellRef;
|
||||||
const { current: attCellContainer } = attCellRef;
|
|
||||||
if (micCellContainer && micCellEl) {
|
if (micCellContainer && micCellEl) {
|
||||||
emptyElement(micCellContainer);
|
emptyElement(micCellContainer);
|
||||||
micCellContainer.appendChild(micCellEl);
|
micCellContainer.appendChild(micCellEl);
|
||||||
}
|
}
|
||||||
if (attCellContainer && attCellEl) {
|
|
||||||
emptyElement(attCellContainer);
|
|
||||||
attCellContainer.appendChild(attCellEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
return noop;
|
return noop;
|
||||||
},
|
},
|
||||||
[micCellRef, attCellRef, micCellEl, attCellEl, large, dirty, showMic]
|
[micCellRef, micCellEl, large, dirty, showMic]
|
||||||
);
|
);
|
||||||
|
|
||||||
React.useLayoutEffect(
|
React.useLayoutEffect(
|
||||||
|
@ -235,8 +231,12 @@ export const CompositionArea = ({
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
const attButtonFragment = (
|
const attButton = (
|
||||||
<div className="module-composition-area__button-cell" ref={attCellRef} />
|
<div className="module-composition-area__button-cell">
|
||||||
|
<div className="choose-file">
|
||||||
|
<button className="paperclip thumbnail" onClick={onChooseAttachment} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const sendButtonFragment = (
|
const sendButtonFragment = (
|
||||||
|
@ -318,13 +318,14 @@ export const CompositionArea = ({
|
||||||
onEditorStateChange={onEditorStateChange}
|
onEditorStateChange={onEditorStateChange}
|
||||||
onDirtyChange={setDirty}
|
onDirtyChange={setDirty}
|
||||||
skinTone={skinTone}
|
skinTone={skinTone}
|
||||||
|
startingText={startingText}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{!large ? (
|
{!large ? (
|
||||||
<>
|
<>
|
||||||
{stickerButtonFragment}
|
{stickerButtonFragment}
|
||||||
{!dirty ? micButtonFragment : null}
|
{!dirty ? micButtonFragment : null}
|
||||||
{attButtonFragment}
|
{attButton}
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
@ -337,7 +338,7 @@ export const CompositionArea = ({
|
||||||
>
|
>
|
||||||
{emojiButtonFragment}
|
{emojiButtonFragment}
|
||||||
{stickerButtonFragment}
|
{stickerButtonFragment}
|
||||||
{attButtonFragment}
|
{attButton}
|
||||||
{!dirty ? micButtonFragment : null}
|
{!dirty ? micButtonFragment : null}
|
||||||
{dirty || !showMic ? sendButtonFragment : null}
|
{dirty || !showMic ? sendButtonFragment : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -38,6 +38,7 @@ export type Props = {
|
||||||
readonly editorRef?: React.RefObject<Editor>;
|
readonly editorRef?: React.RefObject<Editor>;
|
||||||
readonly inputApi?: React.MutableRefObject<InputApi | undefined>;
|
readonly inputApi?: React.MutableRefObject<InputApi | undefined>;
|
||||||
readonly skinTone?: EmojiPickDataType['skinTone'];
|
readonly skinTone?: EmojiPickDataType['skinTone'];
|
||||||
|
readonly startingText?: string;
|
||||||
onDirtyChange?(dirty: boolean): unknown;
|
onDirtyChange?(dirty: boolean): unknown;
|
||||||
onEditorStateChange?(messageText: string, caretLocation: number): unknown;
|
onEditorStateChange?(messageText: string, caretLocation: number): unknown;
|
||||||
onEditorSizeChange?(rect: ContentRect): unknown;
|
onEditorSizeChange?(rect: ContentRect): unknown;
|
||||||
|
@ -141,6 +142,25 @@ const combineRefs = createSelector(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getInitialEditorState = (startingText?: string) => {
|
||||||
|
if (!startingText) {
|
||||||
|
return EditorState.createEmpty(compositeDecorator);
|
||||||
|
}
|
||||||
|
|
||||||
|
const end = startingText.length;
|
||||||
|
const state = EditorState.createWithContent(
|
||||||
|
ContentState.createFromText(startingText),
|
||||||
|
compositeDecorator
|
||||||
|
);
|
||||||
|
const selection = state.getSelection();
|
||||||
|
const selectionAtEnd = selection.merge({
|
||||||
|
anchorOffset: end,
|
||||||
|
focusOffset: end,
|
||||||
|
}) as SelectionState;
|
||||||
|
|
||||||
|
return EditorState.forceSelection(state, selectionAtEnd);
|
||||||
|
};
|
||||||
|
|
||||||
// tslint:disable-next-line max-func-body-length
|
// tslint:disable-next-line max-func-body-length
|
||||||
export const CompositionInput = ({
|
export const CompositionInput = ({
|
||||||
i18n,
|
i18n,
|
||||||
|
@ -154,9 +174,10 @@ export const CompositionInput = ({
|
||||||
onPickEmoji,
|
onPickEmoji,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
skinTone,
|
skinTone,
|
||||||
|
startingText,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [editorRenderState, setEditorRenderState] = React.useState(
|
const [editorRenderState, setEditorRenderState] = React.useState(
|
||||||
EditorState.createEmpty(compositeDecorator)
|
getInitialEditorState(startingText)
|
||||||
);
|
);
|
||||||
const [searchText, setSearchText] = React.useState<string>('');
|
const [searchText, setSearchText] = React.useState<string>('');
|
||||||
const [emojiResults, setEmojiResults] = React.useState<Array<EmojiData>>([]);
|
const [emojiResults, setEmojiResults] = React.useState<Array<EmojiData>>([]);
|
||||||
|
|
|
@ -23,6 +23,9 @@ export type PropsData = {
|
||||||
unreadCount: number;
|
unreadCount: number;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
|
|
||||||
|
draftPreview?: string;
|
||||||
|
shouldShowDraft?: boolean;
|
||||||
|
|
||||||
typingContact?: Object;
|
typingContact?: Object;
|
||||||
lastMessage?: {
|
lastMessage?: {
|
||||||
status: 'sending' | 'sent' | 'delivered' | 'read' | 'error';
|
status: 'sending' | 'sent' | 'delivered' | 'read' | 'error';
|
||||||
|
@ -134,11 +137,23 @@ export class ConversationListItem extends React.PureComponent<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderMessage() {
|
public renderMessage() {
|
||||||
const { lastMessage, typingContact, unreadCount, i18n } = this.props;
|
const {
|
||||||
|
draftPreview,
|
||||||
|
i18n,
|
||||||
|
lastMessage,
|
||||||
|
shouldShowDraft,
|
||||||
|
typingContact,
|
||||||
|
unreadCount,
|
||||||
|
} = this.props;
|
||||||
if (!lastMessage && !typingContact) {
|
if (!lastMessage && !typingContact) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const text = lastMessage && lastMessage.text ? lastMessage.text : '';
|
const text =
|
||||||
|
shouldShowDraft && draftPreview
|
||||||
|
? draftPreview
|
||||||
|
: lastMessage && lastMessage.text
|
||||||
|
? lastMessage.text
|
||||||
|
: '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="module-conversation-list-item__message">
|
<div className="module-conversation-list-item__message">
|
||||||
|
|
|
@ -85,6 +85,9 @@ export class AttachmentList extends React.Component<Props> {
|
||||||
closeButton={true}
|
closeButton={true}
|
||||||
onClick={clickCallback}
|
onClick={clickCallback}
|
||||||
onClickClose={onCloseAttachment}
|
onClickClose={onCloseAttachment}
|
||||||
|
onError={() => {
|
||||||
|
onCloseAttachment(attachment);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,7 @@ type PropsActionsType = {
|
||||||
loadOlderMessages: (messageId: string) => unknown;
|
loadOlderMessages: (messageId: string) => unknown;
|
||||||
loadNewerMessages: (messageId: string) => unknown;
|
loadNewerMessages: (messageId: string) => unknown;
|
||||||
loadNewestMessages: (messageId: string) => unknown;
|
loadNewestMessages: (messageId: string) => unknown;
|
||||||
markMessageRead: (messageId: string) => unknown;
|
markMessageRead: (messageId: string, forceFocus?: boolean) => unknown;
|
||||||
} & MessageActionsType &
|
} & MessageActionsType &
|
||||||
SafetyNumberActionsType;
|
SafetyNumberActionsType;
|
||||||
|
|
||||||
|
@ -397,7 +397,7 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
|
|
||||||
// tslint:disable-next-line member-ordering cyclomatic-complexity
|
// tslint:disable-next-line member-ordering cyclomatic-complexity
|
||||||
public updateWithVisibleRows = debounce(
|
public updateWithVisibleRows = debounce(
|
||||||
() => {
|
(forceFocus?: boolean) => {
|
||||||
const {
|
const {
|
||||||
unreadCount,
|
unreadCount,
|
||||||
haveNewest,
|
haveNewest,
|
||||||
|
@ -421,7 +421,7 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
markMessageRead(newest.id);
|
markMessageRead(newest.id, forceFocus);
|
||||||
|
|
||||||
const rowCount = this.getRowCount();
|
const rowCount = this.getRowCount();
|
||||||
|
|
||||||
|
@ -699,6 +699,22 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
this.updateWithVisibleRows();
|
||||||
|
// @ts-ignore
|
||||||
|
window.registerForFocus(this.forceFocusVisibleRowUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
// @ts-ignore
|
||||||
|
window.unregisterForFocus(this.forceFocusVisibleRowUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public forceFocusVisibleRowUpdate = () => {
|
||||||
|
const forceFocus = true;
|
||||||
|
this.updateWithVisibleRows(forceFocus);
|
||||||
|
};
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: Props) {
|
public componentDidUpdate(prevProps: Props) {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
|
@ -732,8 +748,6 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
if (prevProps.items && prevProps.items.length > 0) {
|
if (prevProps.items && prevProps.items.length > 0) {
|
||||||
this.resizeAll();
|
this.resizeAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
|
||||||
} else if (!typingContact && prevProps.typingContact) {
|
} else if (!typingContact && prevProps.typingContact) {
|
||||||
this.resizeAll();
|
this.resizeAll();
|
||||||
} else if (oldestUnreadIndex !== prevProps.oldestUnreadIndex) {
|
} else if (oldestUnreadIndex !== prevProps.oldestUnreadIndex) {
|
||||||
|
@ -784,6 +798,8 @@ export class Timeline extends React.PureComponent<Props, State> {
|
||||||
clearChangedMessages(id);
|
clearChangedMessages(id);
|
||||||
} else if (this.resizeAllFlag) {
|
} else if (this.resizeAllFlag) {
|
||||||
this.resizeAll();
|
this.resizeAll();
|
||||||
|
} else {
|
||||||
|
this.updateWithVisibleRows();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -63,6 +63,10 @@ export type ConversationType = {
|
||||||
phoneNumber: string;
|
phoneNumber: string;
|
||||||
profileName?: string;
|
profileName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
shouldShowDraft?: boolean;
|
||||||
|
draftText?: string;
|
||||||
|
draftPreview?: string;
|
||||||
};
|
};
|
||||||
export type ConversationLookupType = {
|
export type ConversationLookupType = {
|
||||||
[key: string]: ConversationType;
|
[key: string]: ConversationType;
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { StateType } from '../reducer';
|
||||||
|
|
||||||
import { isShortName } from '../../components/emoji/lib';
|
import { isShortName } from '../../components/emoji/lib';
|
||||||
import { getIntl } from '../selectors/user';
|
import { getIntl } from '../selectors/user';
|
||||||
|
import { getConversationSelector } from '../selectors/conversations';
|
||||||
import {
|
import {
|
||||||
getBlessedStickerPacks,
|
getBlessedStickerPacks,
|
||||||
getInstalledStickerPacks,
|
getInstalledStickerPacks,
|
||||||
|
@ -16,12 +17,25 @@ import {
|
||||||
getRecentStickers,
|
getRecentStickers,
|
||||||
} from '../selectors/stickers';
|
} from '../selectors/stickers';
|
||||||
|
|
||||||
|
type ExternalProps = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
const selectRecentEmojis = createSelector(
|
const selectRecentEmojis = createSelector(
|
||||||
({ emojis }: StateType) => emojis.recents,
|
({ emojis }: StateType) => emojis.recents,
|
||||||
recents => recents.filter(isShortName)
|
recents => recents.filter(isShortName)
|
||||||
);
|
);
|
||||||
|
|
||||||
const mapStateToProps = (state: StateType) => {
|
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
|
const { id } = props;
|
||||||
|
|
||||||
|
const conversation = getConversationSelector(state)(id);
|
||||||
|
if (!conversation) {
|
||||||
|
throw new Error(`Conversation id ${id} not found!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { draftText } = conversation;
|
||||||
|
|
||||||
const receivedPacks = getReceivedStickerPacks(state);
|
const receivedPacks = getReceivedStickerPacks(state);
|
||||||
const installedPacks = getInstalledStickerPacks(state);
|
const installedPacks = getInstalledStickerPacks(state);
|
||||||
const blessedPacks = getBlessedStickerPacks(state);
|
const blessedPacks = getBlessedStickerPacks(state);
|
||||||
|
@ -43,6 +57,7 @@ const mapStateToProps = (state: StateType) => {
|
||||||
return {
|
return {
|
||||||
// Base
|
// Base
|
||||||
i18n: getIntl(state),
|
i18n: getIntl(state),
|
||||||
|
startingText: draftText,
|
||||||
// Emojis
|
// Emojis
|
||||||
recentEmojis,
|
recentEmojis,
|
||||||
skinTone: get(state, ['items', 'skinTone'], 0),
|
skinTone: get(state, ['items', 'skinTone'], 0),
|
||||||
|
|
|
@ -7862,7 +7862,7 @@
|
||||||
"rule": "DOM-innerHTML",
|
"rule": "DOM-innerHTML",
|
||||||
"path": "ts/components/CompositionArea.tsx",
|
"path": "ts/components/CompositionArea.tsx",
|
||||||
"line": " el.innerHTML = '';",
|
"line": " el.innerHTML = '';",
|
||||||
"lineNumber": 64,
|
"lineNumber": 65,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-08-01T14:10:37.481Z",
|
"updated": "2019-08-01T14:10:37.481Z",
|
||||||
"reasonDetail": "Our code, no user input, only clearing out the dom"
|
"reasonDetail": "Our code, no user input, only clearing out the dom"
|
||||||
|
|
|
@ -53,7 +53,6 @@ const excludedFiles = [
|
||||||
'^js/models/messages.js',
|
'^js/models/messages.js',
|
||||||
'^js/modules/crypto.js',
|
'^js/modules/crypto.js',
|
||||||
'^js/views/conversation_view.js',
|
'^js/views/conversation_view.js',
|
||||||
'^js/views/file_input_view.js',
|
|
||||||
'^js/background.js',
|
'^js/background.js',
|
||||||
|
|
||||||
// Generated files
|
// Generated files
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue