Page MenuHomePhorge

D232.1758358503.diff
No OneTemporary

Size
21 KB
Referenced Files
None
Subscribers
None

D232.1758358503.diff

diff --git a/src/contents/ui/FileHandler.qml b/src/contents/ui/FileHandler.qml
--- a/src/contents/ui/FileHandler.qml
+++ b/src/contents/ui/FileHandler.qml
@@ -11,33 +11,24 @@
/**
* Used to handle files described in event, such as caching(if autoCache is true) and download.
- * It is either used for caching or downloading. These two operations cannot be performed in the same FileHandler.
+ * It is either used for caching or downloading. These two operations can be performed in same FileHandler.(Set autoCache to true and then call downloadFile)
+ * But one Filehandler can only track one download/cache job.
*/
QtObject {
id: fileHandler
- required property var eventContent
/**
- * If autoCache is true, the file is automatically cached(thumbnail is downloaded if exists).
+ * Just set these four properties, any other properties will be automatically evaluated.
+ * The eventContent can be modified at any time.
+ * Modifying other properties can lead to unpredictable behavior.
*/
- required property bool autoCache
+ required property var eventContent
+ required property bool autoCache // thumbnail(if exists) will be downloaded if set to true
required property var kazvIOManager
required property var matrixSdk
- /**
- * Used to download or caching.
- * mxcUri, sha256, key, iv will be setted automatically
- * according to whether the file is encrypted or not.
- */
- property var mxcUri: ""
property var httpVer: matrixSdk.checkSpecVersion("v1.11") ? MK.MatrixSdk.AuthenticatedMediaV1 : MK.MatrixSdk.UnauthenticatedMediaV3
- property var url: fileHandler.mxcToHttp(httpVer, mxcUri)
- property var saveFileUrl: ""
- property var mediaId: mxcUri && fileHandler.getMediaId(fileHandler.mxcUri)
- property var sha256: ""
- property var token: httpVer === MK.MatrixSdk.AuthenticatedMediaV1 ? matrixSdk.token : ""
- property var key: ""
- property var iv: ""
+ property var token: (httpVer === MK.MatrixSdk.AuthenticatedMediaV1) ? matrixSdk.token : ""
property var kazvIOJob
readonly property url localFile: kazvIOJob ? '' : cachedFile
@@ -50,25 +41,24 @@
*/
property url cachedFile
- property var fileInfo: "info" in eventContent ? eventContent.info : {}
+ property var encryptedFileMxcUri
+ property var encryptedFileSha256
+ property var encryptedFileKey
+ property var encryptedFileIv
- property var encryptedFile: "file" in eventContent ? eventContent.file : {}
- property var encryptedFileMxcUri: "url" in encryptedFile ? encryptedFile.url : ""
- property var encryptedFileSha256: "hashes" in encryptedFile && "sha256" in encryptedFile.hashes ? encryptedFile.hashes.sha256 : ""
- // property var encryptedFileAlg: "key" in encryptedFile && "alg" in encryptedFile.key ? encryptedFile.key.alg : ""
- property var encryptedFileKey: "key" in encryptedFile && "k" in encryptedFile.key ? encryptedFile.key.k : ""
- property var encryptedFileIv: "iv" in encryptedFile ? encryptedFile.iv : ""
+ property var encryptedThumbnailMxcUri
+ property var encryptedThumbnailSha256
+ property var encryptedThumbnailKey
+ property var encryptedThumbnailIv
- property var encryptedThumbnail: "thumbnail_file" in fileInfo ? fileInfo.thumbnail_file : {}
- property var encryptedThumbnailMxcUri: "url" in encryptedThumbnail ? encryptedThumbnail.url : ""
- property var encryptedThumbnailSha256: "hashes" in encryptedThumbnail && "sha256" in encryptedThumbnail.hashes ? encryptedThumbnail.hashes.sha256 : ""
- // property var encryptedFileAlg: "key" in encryptedThumbnail && "alg" in encryptedThumbnail.key ? encryptedThumbnail.key.alg : ""
- property var encryptedThumbnailKey: "key" in encryptedThumbnail && "k" in encryptedThumbnail.key ? encryptedThumbnail.key.k : ""
- property var encryptedThumbnailIv: "iv" in encryptedThumbnail ? encryptedThumbnail.iv : ""
+ property var unencryptedFileMxcUri
- property var unencryptedFileMxcUri: "url" in eventContent ? eventContent.url : ""
+ property var unencryptedThumbnailMxcUri
- property var unencryptedThumbnailMxcUri: "thumbnail_url" in fileInfo ? fileInfo.thumbnail_url : ""
+ property bool isValid
+ property bool isEncrypted
+ property bool hasEncryptedThumbnail
+ property bool hasUnencryptedThumbnail
/**
* Emit when start download, not emit when start cache.
@@ -82,27 +72,51 @@
}
function downloadFile(saveFileUrl) {
- fileHandler.saveFileUrl = saveFileUrl
- fileHandler.kazvIOManager.startNewDownloadJob(fileHandler.url,
- fileHandler.saveFileUrl,
- fileHandler.mediaId,
- fileHandler.sha256,
- fileHandler.token,
- fileHandler.key,
- fileHandler.iv)
- fileHandler.updateKazvIOJob(fileHandler.mediaId)
-
- fileHandler.startDownload()
+ if (!isValid) {
+ return;
+ }
+ const mxcUri = isEncrypted ? encryptedFileMxcUri : unencryptedFileMxcUri;
+ const mediaId = getMediaId(mxcUri);
+ const sha256 = isEncrypted ? encryptedFileSha256 : "";
+ const key = isEncrypted ? encryptedFileKey : "";
+ const iv = isEncrypted ? encryptedFileIv : "";
+
+ kazvIOManager.startNewDownloadJob(mxcToHttp(httpVer, mxcUri), saveFileUrl, mediaId,
+ sha256, token, key, iv);
+ fileHandler.updateKazvIOJob(mediaId);
+
+ fileHandler.startDownload();
}
function cacheFile() {
- fileHandler.cachedFile = fileHandler.kazvIOManager.cacheFile(fileHandler.url,
- fileHandler.mediaId,
- fileHandler.sha256,
- fileHandler.token,
- fileHandler.key,
- fileHandler.iv)
- fileHandler.updateKazvIOJob(fileHandler.mediaId)
+ if (!isValid) {
+ return;
+ }
+ let mxcUri = "";
+ let sha256 = "";
+ let key = "";
+ let iv = "";
+ // Prefer thumbnail and prefer encrypted
+ if (hasEncryptedThumbnail) {
+ mxcUri = encryptedThumbnailMxcUri;
+ sha256 = encryptedThumbnailSha256;
+ key = encryptedThumbnailKey;
+ iv = encryptedThumbnailIv;
+ } else if (hasUnencryptedThumbnail) {
+ mxcUri = unencryptedThumbnailMxcUri;
+ } else if (isEncrypted) {
+ mxcUri = encryptedFileMxcUri;
+ sha256 = encryptedFileSha256;
+ key = encryptedFileKey;
+ iv = encryptedFileIv;
+ } else {
+ mxcUri = unencryptedFileMxcUri;
+ }
+ const mediaId = getMediaId(mxcUri);
+
+ fileHandler.cachedFile = kazvIOManager.cacheFile(mxcToHttp(httpVer, mxcUri), mediaId,
+ sha256, token, key, iv);
+ fileHandler.updateKazvIOJob(fileHandler.mediaId);
}
function updateKazvIOJob(mediaId) {
@@ -132,45 +146,41 @@
}
}
- Component.onCompleted: {
- // Check if there is an encryted file or unencrypted file
- if (encryptedFileMxcUri &&
- encryptedFileSha256 &&
- encryptedFileKey &&
- encryptedFileIv) {
- fileHandler.mxcUri = encryptedFileMxcUri
- fileHandler.sha256 = encryptedFileSha256
- fileHandler.key = encryptedFileKey
- fileHandler.iv = encryptedFileIv
- } else if (unencryptedFileMxcUri) {
- fileHandler.mxcUri = unencryptedFileMxcUri
- } else {
- // The event content is incorrect or the FileHandler.qml has a bug
- return
- }
+ onEventContentChanged: {
+ // Encrypted file
+ const encryptedFile = eventContent?.file;
+ fileHandler.encryptedFileMxcUri = encryptedFile?.url;
+ fileHandler.encryptedFileSha256 = encryptedFile?.hashes?.sha256;
+ fileHandler.encryptedFileKey = encryptedFile?.key?.k;
+ fileHandler.encryptedFileIv = encryptedFile?.iv;
+
+ // Unencrypted file
+ fileHandler.unencryptedFileMxcUri = eventContent?.url;
+
+ // Encrypted thumbnail file
+ const encryptedThumbnailFile = eventContent?.info?.thumbnail_file;
+ fileHandler.encryptedThumbnailMxcUri = encryptedThumbnailFile?.url;
+ fileHandler.encryptedThumbnailSha256 = encryptedThumbnailFile?.hashes?.sha256;
+ fileHandler.encryptedThumbnailKey = encryptedThumbnailFile?.key?.k;
+ fileHandler.encryptedThumbnailIv = encryptedThumbnailFile?.iv;
+
+ // Unencrypted thumbnail file
+ fileHandler.unencryptedThumbnailMxcUri = eventContent?.info?.thumbnail_url;
+
+ fileHandler.isEncrypted = !!(encryptedFileMxcUri && encryptedFileSha256 && encryptedFileKey && encryptedFileIv);
+ fileHandler.hasEncryptedThumbnail = !!(encryptedThumbnailMxcUri && encryptedThumbnailSha256 && encryptedThumbnailKey && encryptedThumbnailIv);
+ fileHandler.hasUnencryptedThumbnail = !!unencryptedThumbnailMxcUri;
+ // There is at least one file that can be downloaded
+ fileHandler.isValid = !!(isEncrypted || unencryptedFileMxcUri);
- /**
- * Thumbnail will only be downloaded in the cache
- **/
if (fileHandler.autoCache) {
- // Check if there is an encrypted thumbnail or unencrypted thumbnail
- if (encryptedThumbnailMxcUri &&
- encryptedThumbnailSha256 &&
- encryptedThumbnailKey &&
- encryptedThumbnailIv) {
- fileHandler.mxcUri = encryptedThumbnailMxcUri
- fileHandler.sha256 = encryptedThumbnailSha256
- fileHandler.key = encryptedThumbnailKey
- fileHandler.iv = encryptedThumbnailIv
- } else if (unencryptedThumbnailMxcUri) {
- fileHandler.mxcUri = unencryptedThumbnailMxcUri
- }
- fileHandler.cacheFile()
+ fileHandler.cacheFile();
}
- fileHandler.updateKazvIOJob(fileHandler.mediaId)
+ // Check if the file is downloading
+ fileHandler.updateKazvIOJob(fileHandler.mediaId);
fileHandler.onSuccessChanged.connect(function () {
- fileHandler.kazvIOManager.deleteDownloadJob(mediaId)
- })
+ fileHandler.kazvIOManager.deleteDownloadJob(mediaId);
+ });
}
}
diff --git a/src/tests/quick-tests/tst_FileHandler.qml b/src/tests/quick-tests/tst_FileHandler.qml
--- a/src/tests/quick-tests/tst_FileHandler.qml
+++ b/src/tests/quick-tests/tst_FileHandler.qml
@@ -11,20 +11,31 @@
import 'test-helpers' as QmlHelpers
import 'test-helpers.js' as JsHelpers
+import moe.kazv.mxc.kazv as MK
+
QmlHelpers.TestItem {
id: item
property var unencryptedEventContent: ({
body: "fileName",
msgtype: "m.image",
- url: "mxc://some",
+ url: "mxc://someunencrypted",
+ })
+
+ property var unencryptedThumbnailEventContent: ({
+ body: "fileName",
+ msgtype: "m.image",
+ url: "mxc://someunencrypted",
+ info: {
+ thumbnail_url: "mxc://someunencryptedthumbnail"
+ }
})
property var encryptedEventContent: ({
body: "fileName",
msgtype: "m.image",
file: {
- url: "mxc://some",
+ url: "mxc://someencrypted",
key: {
kty: "oct",
key_ops: ["encrypt","decrypt"],
@@ -40,37 +51,59 @@
}
})
- property var oldFileHandler: Kazv.FileHandler {
- eventContent: unencryptedEventContent
- autoCache: true
- kazvIOManager: QmlHelpers.KazvIOManagerMock {}
- matrixSdk: QmlHelpers.MatrixSdkMock {
- specVersions: []
- property var mxcUriToHttp: mockHelper.noop()
+ property var encryptedThumbnailEventContent: ({
+ body: "fileName",
+ msgtype: "m.image",
+ file: {
+ url: "mxc://someencrypted",
+ key: {
+ kty: "oct",
+ key_ops: ["encrypt","decrypt"],
+ alg: "A256CTR",
+ k: "somekey",
+ ext: true,
+ },
+ iv: "someiv",
+ hashes: {
+ "sha256": "somesha256hash"
+ },
+ v: "v2",
+ },
+ info: {
+ thumbnail_file: {
+ url: "mxc://someencryptedthumbnail",
+ key: {
+ kty: "oct",
+ key_ops: ["encrypt","decrypt"],
+ alg: "A256CTR",
+ k: "somekeyofthumbnail",
+ ext: true,
+ },
+ iv: "someivofthumbnail",
+ hashes: {
+ "sha256": "somesha256hashofthumbnail"
+ },
+ v: "v2",
+ }
}
- }
+ })
- property var newFileHandler: Kazv.FileHandler {
- eventContent: unencryptedEventContent
- autoCache: true
- kazvIOManager: QmlHelpers.KazvIOManagerMock {}
- matrixSdk: QmlHelpers.MatrixSdkMock {
- property var mxcUriToHttpAuthenticatedV1: mockHelper.noop()
- }
+ property var kazvIOManager: QmlHelpers.KazvIOManagerMock {}
+ property var matrixSdk: QmlHelpers.MatrixSdkMock {}
+ property var matrixSdkUnauthenticatedMediaV3: QmlHelpers.MatrixSdkMock {
+ specVersions: []
+ property var mxcUriToHttp: mockHelper.noop()
}
-
- property var unencryptedFileHandler: Kazv.FileHandler {
- eventContent: unencryptedEventContent
- autoCache: true
- kazvIOManager: QmlHelpers.KazvIOManagerMock {}
- matrixSdk: QmlHelpers.MatrixSdkMock {}
+ property var matrixSdkAuthenticatedMediaV1: QmlHelpers.MatrixSdkMock {
+ property var mxcUriToHttpAuthenticatedV1: mockHelper.noop()
}
- property var encryptedFileHandler: Kazv.FileHandler {
- eventContent: encryptedEventContent
- autoCache: true
- kazvIOManager: QmlHelpers.KazvIOManagerMock {}
- matrixSdk: QmlHelpers.MatrixSdkMock {}
+
+ property var fileHandler: Kazv.FileHandler {
+ eventContent: ({})
+ autoCache: false
+ kazvIOManager: item.kazvIOManager
+ matrixSdk: item.matrixSdk
}
TestCase {
@@ -78,52 +111,137 @@
name: 'fileHandlerTest'
when: windowShown
- function test_specVersion() {
- compare(oldFileHandler.matrixSdk.mxcUriToHttp.calledTimes(), 1);
- compare(newFileHandler.matrixSdk.mxcUriToHttpAuthenticatedV1.calledTimes(), 1);
-
- compare(oldFileHandler.kazvIOManager.cacheFile.calledTimes(), 1);
- compare(oldFileHandler.kazvIOManager.cacheFile.lastArgs()[3], "");
- compare(newFileHandler.kazvIOManager.cacheFile.calledTimes(), 1);
- compare(newFileHandler.kazvIOManager.cacheFile.lastArgs()[3], newFileHandler.matrixSdk.token);
-
- oldFileHandler.downloadFile("fileUrl");
- compare(oldFileHandler.kazvIOManager.startNewDownloadJob.calledTimes(), 1);
- compare(oldFileHandler.kazvIOManager.startNewDownloadJob.lastArgs()[4], "");
- newFileHandler.downloadFile("fileUrl");
- compare(newFileHandler.kazvIOManager.startNewDownloadJob.calledTimes(), 1);
- compare(newFileHandler.kazvIOManager.startNewDownloadJob.lastArgs()[4], newFileHandler.matrixSdk.token);
+ function init() {
+ fileHandler.autoCache = false;
+ fileHandler.eventContent = ({});
+ fileHandler.matrixSdk = item.matrixSdk;
+ mockHelper.clearAll();
+ }
+
+ // https://spec.matrix.org/v1.16/client-server-api/#get_matrixmediav3downloadservernamemediaid
+ function test_unauthenticatedMediaV3() {
+ fileHandler.matrixSdk = matrixSdkUnauthenticatedMediaV3;
+ fileHandler.autoCache = true;
+ fileHandler.eventContent = unencryptedEventContent;
+
+ compare(fileHandler.httpVer, MK.MatrixSdk.UnauthenticatedMediaV3);
+ compare(matrixSdkUnauthenticatedMediaV3.mxcUriToHttp.calledTimes(), 1);
+ compare(matrixSdkUnauthenticatedMediaV3.mxcUriToHttp.lastArgs()[0], "mxc://someunencrypted");
+ compare(kazvIOManager.cacheFile.calledTimes(), 1);
+ compare(kazvIOManager.cacheFile.lastArgs()[3], "");
+
+ fileHandler.downloadFile("fileUrl");
+ compare(kazvIOManager.startNewDownloadJob.calledTimes(), 1);
+ compare(kazvIOManager.startNewDownloadJob.lastArgs()[4], "");
+ }
+
+ // https://spec.matrix.org/v1.16/client-server-api/#get_matrixclientv1mediadownloadservernamemediaid
+ function test_authenticatedMediaV1() {
+ fileHandler.matrixSdk = matrixSdkAuthenticatedMediaV1;
+ fileHandler.autoCache = true;
+ fileHandler.eventContent = unencryptedEventContent;
+
+ compare(fileHandler.httpVer, MK.MatrixSdk.AuthenticatedMediaV1);
+ compare(fileHandler.matrixSdk.mxcUriToHttpAuthenticatedV1.calledTimes(), 1);
+ compare(fileHandler.matrixSdk.mxcUriToHttpAuthenticatedV1.lastArgs()[0], "mxc://someunencrypted");
+ compare(kazvIOManager.cacheFile.calledTimes(), 1);
+ compare(kazvIOManager.cacheFile.lastArgs()[3], fileHandler.matrixSdk.token);
+
+ fileHandler.downloadFile("fileUrl");
+ compare(kazvIOManager.startNewDownloadJob.calledTimes(), 1);
+ compare(kazvIOManager.startNewDownloadJob.lastArgs()[4], fileHandler.matrixSdk.token);
+ }
+
+ function test_autoCacheEncrypted() {
+ fileHandler.autoCache = true;
+ fileHandler.eventContent = encryptedEventContent;
+ const cacheFunc = kazvIOManager.cacheFile;
+ compare(cacheFunc.calledTimes(), 1);
+ compare(cacheFunc.lastArgs()[0],
+ matrixSdk.mxcUriToHttpAuthenticatedV1("mxc://someencrypted"));
+ compare(cacheFunc.lastArgs()[2], "somesha256hash");
+ compare(cacheFunc.lastArgs()[4], "somekey");
+ compare(cacheFunc.lastArgs()[5], "someiv");
+ }
+
+ function test_autoCacheUnencrypted() {
+ fileHandler.autoCache = true;
+ fileHandler.eventContent = unencryptedEventContent;
+ const cacheFunc = kazvIOManager.cacheFile;
+ compare(cacheFunc.calledTimes(), 1);
+ compare(cacheFunc.lastArgs()[0],
+ matrixSdk.mxcUriToHttpAuthenticatedV1("mxc://someunencrypted"));
+ }
+
+ // Prefer thumbnail when caching
+ function test_autoCacheThumbnail() {
+ fileHandler.autoCache = true;
+ fileHandler.eventContent = unencryptedThumbnailEventContent;
+ const cacheFunc = kazvIOManager.cacheFile;
+ compare(cacheFunc.calledTimes(), 1);
+ compare(cacheFunc.lastArgs()[0],
+ matrixSdk.mxcUriToHttpAuthenticatedV1("mxc://someunencryptedthumbnail"));
}
- function test_autoCache() {
- compare(unencryptedFileHandler.kazvIOManager.cacheFile.calledTimes(), 1);
- compare(encryptedFileHandler.kazvIOManager.cacheFile.calledTimes(), 1);
+ function test_autoCacheEncryptedThumbnail() {
+ fileHandler.autoCache = true;
+ fileHandler.eventContent = encryptedThumbnailEventContent;
+ const cacheFunc = kazvIOManager.cacheFile;
+ compare(cacheFunc.calledTimes(), 1);
+ compare(cacheFunc.lastArgs()[0],
+ matrixSdk.mxcUriToHttpAuthenticatedV1("mxc://someencryptedthumbnail"));
+ compare(cacheFunc.lastArgs()[2], "somesha256hashofthumbnail");
+ compare(cacheFunc.lastArgs()[4], "somekeyofthumbnail");
+ compare(cacheFunc.lastArgs()[5], "someivofthumbnail");
}
- function test_encrypted() {
- let downloadFunc = encryptedFileHandler.kazvIOManager.startNewDownloadJob;
- encryptedFileHandler.downloadFile("fileUrl");
+ function test_downloadEncrypted() {
+ fileHandler.eventContent = encryptedEventContent;
+ fileHandler.downloadFile("fileUrl");
+ const downloadFunc = kazvIOManager.startNewDownloadJob;
compare(downloadFunc.calledTimes(), 1);
- compare(downloadFunc.lastArgs()[0], encryptedFileHandler.matrixSdk.mxcUriToHttpAuthenticatedV1("mxc://some"));
+ compare(downloadFunc.lastArgs()[0], matrixSdk.mxcUriToHttpAuthenticatedV1("mxc://someencrypted"));
compare(downloadFunc.lastArgs()[1], "fileUrl");
compare(downloadFunc.lastArgs()[3], "somesha256hash");
- compare(downloadFunc.lastArgs()[4], encryptedFileHandler.matrixSdk.token);
compare(downloadFunc.lastArgs()[5], "somekey");
compare(downloadFunc.lastArgs()[6], "someiv");
}
- function test_unencrypted() {
- let downloadFunc = unencryptedFileHandler.kazvIOManager.startNewDownloadJob;
- unencryptedFileHandler.downloadFile("fileUrl");
+ function test_downloadUnencrypted() {
+ fileHandler.eventContent = unencryptedEventContent;
+ const downloadFunc = kazvIOManager.startNewDownloadJob;
+ fileHandler.downloadFile("fileUrl");
compare(downloadFunc.calledTimes(), 1);
- let args = downloadFunc.lastArgs();
- compare(args[0], unencryptedFileHandler.matrixSdk.mxcUriToHttpAuthenticatedV1("mxc://some"));
- compare(args[1], "fileUrl");
- compare(args[3], "");
- compare(args[4], unencryptedFileHandler.matrixSdk.token);
- compare(args[5], "");
- compare(args[6], "");
+ const args = downloadFunc.lastArgs();
+ compare(downloadFunc.lastArgs()[0], matrixSdk.mxcUriToHttpAuthenticatedV1("mxc://someunencrypted"));
+ compare(downloadFunc.lastArgs()[1], "fileUrl");
+ compare(downloadFunc.lastArgs()[3], "");
+ compare(downloadFunc.lastArgs()[5], "");
+ compare(downloadFunc.lastArgs()[6], "");
+ }
+
+ function test_eventContentChanged() {
+ fileHandler.eventContent = encryptedEventContent;
+ compare(fileHandler.encryptedFileMxcUri, "mxc://someencrypted");
+ compare(fileHandler.encryptedFileSha256, "somesha256hash");
+ compare(fileHandler.encryptedFileKey, "somekey");
+ compare(fileHandler.encryptedFileIv, "someiv");
+
+ fileHandler.eventContent = unencryptedEventContent;
+ compare(fileHandler.unencryptedFileMxcUri, "mxc://someunencrypted");
+ compare(fileHandler.encryptedFileMxcUri, undefined);
+ compare(fileHandler.encryptedFileSha256, undefined);
+ compare(fileHandler.encryptedFileKey, undefined);
+ compare(fileHandler.encryptedFileIv, undefined);
+ }
+
+ function test_invalidEventContent() {
+ fileHandler.autoCache = true;
+ fileHandler.eventContent = ({});
+ compare(kazvIOManager.cacheFile.calledTimes(), 0);
+ fileHandler.downloadFile("fileUrl");
+ compare(kazvIOManager.startNewDownloadJob.calledTimes(), 0);
}
}
}

File Metadata

Mime Type
text/plain
Expires
Sat, Sep 20, 1:55 AM (21 h, 52 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
464216
Default Alt Text
D232.1758358503.diff (21 KB)

Event Timeline