@atlaskit/media-viewer 54.3.7 → 54.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # @atlaskit/media-viewer
2
2
 
3
+ ## 54.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`ecf905d329e42`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/ecf905d329e42) -
8
+ Add diagnostic context to the imageviewer-src-onerror failure (errorDetail now report mimeType,
9
+ decode dimensions and failed source type instead of unknown)
10
+ - [`a654b7573d0a2`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/a654b7573d0a2) -
11
+ Route image MIME types that the browser cannot natively decode (e.g. HEIC/HEIF, PSD, TIFF) to the
12
+ unsupported/download preview view instead of a guaranteed-failing `<img>`. These are reported via
13
+ the `previewUnsupported` analytics event rather than `loadFailed`. Behind the
14
+ `platform_media_unsupported_mime_routing` feature gate.
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies
19
+
20
+ ## 54.3.8
21
+
22
+ ### Patch Changes
23
+
24
+ - [`bea4da79c297d`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/bea4da79c297d) -
25
+ [ux] EDITOR-7679 use fallback media name for filename in download
26
+ - Updated dependencies
27
+
3
28
  ## 54.3.7
4
29
 
5
30
  ### Patch Changes
@@ -10,7 +10,7 @@ exports.packageVersion = exports.packageName = void 0;
10
10
  var _analytics = require("@atlaskit/media-common/analytics");
11
11
  var componentName = exports.component = exports.componentName = 'mediaViewer';
12
12
  var packageName = exports.packageName = "@atlaskit/media-viewer";
13
- var packageVersion = exports.packageVersion = "54.3.6";
13
+ var packageVersion = exports.packageVersion = "54.4.0";
14
14
  function getFileAttributes(fileState) {
15
15
  if (!fileState) {
16
16
  return {
@@ -13,7 +13,7 @@ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
13
13
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
14
14
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
15
15
  var packageName = "@atlaskit/media-viewer";
16
- var packageVersion = "54.3.6";
16
+ var packageVersion = "54.4.0";
17
17
  var ufoExperience;
18
18
  var getExperience = function getExperience() {
19
19
  if (!ufoExperience) {
@@ -22,6 +22,7 @@ var _download2 = require("./analytics/events/operational/download");
22
22
  var _analytics = require("./analytics");
23
23
  var _failedPreviewDownloadButtonClicked = require("./analytics/events/ui/failedPreviewDownloadButtonClicked");
24
24
  var _styleWrappers = require("./styleWrappers");
25
+ var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals");
25
26
  var _errors = require("./errors");
26
27
  var _tooltip = _interopRequireDefault(require("@atlaskit/tooltip"));
27
28
  var _abuseModal = require("@atlaskit/media-ui/abuseModal");
@@ -56,13 +57,14 @@ function DownloadButton(_ref) {
56
57
  }
57
58
  var createItemDownloader = function createItemDownloader(file, mediaClient, options) {
58
59
  return /*#__PURE__*/(0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee() {
59
- var collectionName, traceContext, createAnalyticsEvent, id, name;
60
+ var collectionName, traceContext, createAnalyticsEvent, fallbackMediaName, id, fileStateName, name;
60
61
  return _regenerator.default.wrap(function (_context) {
61
62
  while (1) switch (_context.prev = _context.next) {
62
63
  case 0:
63
- collectionName = options.collectionName, traceContext = options.traceContext, createAnalyticsEvent = options.createAnalyticsEvent;
64
+ collectionName = options.collectionName, traceContext = options.traceContext, createAnalyticsEvent = options.createAnalyticsEvent, fallbackMediaName = options.fallbackMediaName;
64
65
  id = file.id;
65
- name = !(0, _mediaClient.isErrorFileState)(file) ? file.name : undefined;
66
+ fileStateName = !(0, _mediaClient.isErrorFileState)(file) ? file.name : undefined;
67
+ name = fileStateName || ((0, _expValEquals.expValEquals)('platform_editor_media_download_fallback_name', 'isEnabled', true) ? fallbackMediaName : undefined);
66
68
  mediaClient.file.downloadBinary(id, name, collectionName, traceContext).then(function () {
67
69
  (0, _analytics.fireAnalytics)((0, _download2.createDownloadSucceededEventPayload)(file, traceContext), createAnalyticsEvent);
68
70
  }).catch(function (e) {
@@ -94,7 +96,8 @@ var DownloadItem = function DownloadItem(_ref3) {
94
96
  analyticspayload = _ref3.analyticspayload,
95
97
  traceContext = _ref3.traceContext,
96
98
  iconBefore = _ref3.iconBefore,
97
- children = _ref3.children;
99
+ children = _ref3.children,
100
+ fallbackMediaName = _ref3.fallbackMediaName;
98
101
  var _useState = (0, _react.useState)(false),
99
102
  _useState2 = (0, _slicedToArray2.default)(_useState, 2),
100
103
  isAbuseModalOpen = _useState2[0],
@@ -108,7 +111,8 @@ var DownloadItem = function DownloadItem(_ref3) {
108
111
  var itemDownloader = createItemDownloader(fileState, mediaClient, {
109
112
  collectionName: collectionName,
110
113
  createAnalyticsEvent: createAnalyticsEvent,
111
- traceContext: traceContext
114
+ traceContext: traceContext,
115
+ fallbackMediaName: fallbackMediaName
112
116
  });
113
117
  return /*#__PURE__*/_react.default.createElement(_react.default.Fragment, null, shouldRenderAbuseModal && /*#__PURE__*/_react.default.createElement(_abuseModal.AbuseModal, {
114
118
  isOpen: isAbuseModalOpen,
@@ -154,7 +158,8 @@ var ToolbarDownloadButton = exports.ToolbarDownloadButton = function ToolbarDown
154
158
  var state = _ref5.state,
155
159
  mediaClient = _ref5.mediaClient,
156
160
  identifier = _ref5.identifier,
157
- traceContext = _ref5.traceContext;
161
+ traceContext = _ref5.traceContext,
162
+ fallbackMediaName = _ref5.fallbackMediaName;
158
163
  // TODO [MS-1731]: make it work for external files as well
159
164
  if ((0, _mediaClient.isExternalImageIdentifier)(identifier)) {
160
165
  return null;
@@ -168,7 +173,8 @@ var ToolbarDownloadButton = exports.ToolbarDownloadButton = function ToolbarDown
168
173
  mediaClient: mediaClient,
169
174
  collectionName: identifier.collectionName,
170
175
  traceContext: traceContext,
171
- iconBefore: downloadIcon
176
+ iconBefore: downloadIcon,
177
+ fallbackMediaName: fallbackMediaName
172
178
  });
173
179
  };
174
180
  var DisabledToolbarDownloadButton = exports.DisabledToolbarDownloadButton = /*#__PURE__*/_react.default.createElement(_mediaUi.MediaButton, {
@@ -5,12 +5,12 @@ Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
7
  exports.getErrorMessageFromError = exports.errorReasonToMessages = exports.default = exports.ErrorMessage = void 0;
8
- var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
9
8
  var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
10
9
  var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
11
10
  var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));
12
11
  var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf"));
13
12
  var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits"));
13
+ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
14
14
  var _react = _interopRequireDefault(require("react"));
15
15
  var _reactIntl = require("react-intl");
16
16
  var _mediaUi = require("@atlaskit/media-ui");
@@ -51,11 +51,12 @@ var ErrorMessage = exports.ErrorMessage = /*#__PURE__*/function (_React$Componen
51
51
  var _this$props = this.props,
52
52
  formatMessage = _this$props.intl.formatMessage,
53
53
  error = _this$props.error;
54
- // Non-ZIP archives (e.g. RAR, TAR, 7z) aren't a load failure - the
54
+ // Non-ZIP archives (e.g. RAR, TAR, 7z) and image MIME types that no browser
55
+ // can natively decode (e.g. HEIC/HEIF, PSD, TIFF) aren't load failures - the
55
56
  // browser-based viewer simply can't preview them. Show a dedicated
56
57
  // "Unsupported file format" heading instead of the generic
57
58
  // "Something went wrong" copy.
58
- if ((0, _errors.getPrimaryErrorReason)(error) === 'archiveviewer-not-zip') {
59
+ if ((0, _errors.getPrimaryErrorReason)(error) === 'archiveviewer-not-zip' || (0, _errors.getPrimaryErrorReason)(error) === 'imageviewer-unsupported-mime') {
59
60
  return {
60
61
  icon: errorLoadingFileImage(formatMessage),
61
62
  messages: [_mediaUi.messages.unsupported_file_format, _mediaUi.messages.archive_format_not_supported]
@@ -95,6 +96,10 @@ var ErrorMessage = exports.ErrorMessage = /*#__PURE__*/function (_React$Componen
95
96
  (0, _ufoExperiences.failMediaFileUfoExperience)(failMediaFileUfoExperiencePayload);
96
97
  }
97
98
  }
99
+
100
+ // Error reasons that represent a format the browser-based viewer simply can't
101
+ // preview (not a load failure). These are reported as the informational,
102
+ // non-SLI `previewUnsupported` metric instead of `loadFailed`.
98
103
  }, {
99
104
  key: "render",
100
105
  value: function render() {
@@ -112,10 +117,16 @@ var ErrorMessage = exports.ErrorMessage = /*#__PURE__*/function (_React$Componen
112
117
  })), this.props.children);
113
118
  }
114
119
  }], [{
120
+ key: "isPreviewUnsupported",
121
+ value: function isPreviewUnsupported(error) {
122
+ return ErrorMessage.previewUnsupportedReasons.includes((0, _errors.getPrimaryErrorReason)(error));
123
+ }
124
+ }, {
115
125
  key: "getEventPayload",
116
126
  value: function getEventPayload(error, fileId, fileState, traceContext) {
117
- if (fileState && (0, _errors.getPrimaryErrorReason)(error) === 'unsupported') {
118
- // this is not an SLI, its just a useful metric for unsupported
127
+ if (fileState && ErrorMessage.isPreviewUnsupported(error)) {
128
+ // this is not an SLI (load failure), its just a useful metric for files
129
+ // whose format the browser-based viewer can't preview.
119
130
  return (0, _previewUnsupported.createPreviewUnsupportedEvent)(fileState);
120
131
  } else {
121
132
  return (0, _loadFailed.createLoadFailedEvent)(fileId, error, fileState, traceContext);
@@ -123,5 +134,6 @@ var ErrorMessage = exports.ErrorMessage = /*#__PURE__*/function (_React$Componen
123
134
  }
124
135
  }]);
125
136
  }(_react.default.Component); // @ts-ignore: [PIT-1685] Fails in post-office due to backwards incompatibility issue with React 18
137
+ (0, _defineProperty2.default)(ErrorMessage, "previewUnsupportedReasons", ['unsupported', 'imageviewer-unsupported-mime']);
126
138
  var ErroMsg = (0, _analyticsNext.withAnalyticsEvents)()((0, _reactIntl.injectIntl)(ErrorMessage));
127
139
  var _default = exports.default = ErroMsg;
@@ -5,12 +5,15 @@ Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
7
  exports.MediaViewerError = exports.ArchiveViewerError = void 0;
8
+ exports.buildImgErrorDiagnostics = buildImgErrorDiagnostics;
9
+ exports.classifyFailedSrc = classifyFailedSrc;
8
10
  exports.getErrorDetail = getErrorDetail;
9
11
  exports.getPrimaryErrorReason = getPrimaryErrorReason;
10
12
  exports.getRequestMetadata = getRequestMetadata;
11
13
  exports.getSecondaryErrorReason = getSecondaryErrorReason;
12
14
  exports.isArchiveViewerError = isArchiveViewerError;
13
15
  exports.isMediaViewerError = isMediaViewerError;
16
+ var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
14
17
  var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
15
18
  var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
16
19
  var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));
@@ -18,6 +21,7 @@ var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/ge
18
21
  var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits"));
19
22
  var _wrapNativeSuper2 = _interopRequireDefault(require("@babel/runtime/helpers/wrapNativeSuper"));
20
23
  var _mediaClient = require("@atlaskit/media-client");
24
+ var _isMimeTypeSupportedByBrowser = require("@atlaskit/media-common/isMimeTypeSupportedByBrowser");
21
25
  function _callSuper(t, o, e) { return o = (0, _getPrototypeOf2.default)(o), (0, _possibleConstructorReturn2.default)(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], (0, _getPrototypeOf2.default)(t).constructor) : o.apply(t, e)); }
22
26
  function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); }
23
27
  var MediaViewerError = exports.MediaViewerError = /*#__PURE__*/function (_Error) {
@@ -91,4 +95,64 @@ function getRequestMetadata(error) {
91
95
  if ((0, _mediaClient.isCommonMediaClientError)(secondaryError)) {
92
96
  return secondaryError.metadata;
93
97
  }
98
+ }
99
+
100
+ /**
101
+ * Classifies the failed `<img>` source into a coarse, non-sensitive enum so we can
102
+ * tell which kind of source failed without leaking the (potentially signed) url.
103
+ */
104
+ function classifyFailedSrc(currentSrc) {
105
+ if (!currentSrc) {
106
+ return 'unknown';
107
+ }
108
+ if (currentSrc.startsWith('blob:')) {
109
+ return 'blob';
110
+ }
111
+ if (currentSrc.startsWith('data:')) {
112
+ return 'data';
113
+ }
114
+ if (currentSrc.startsWith('http:') || currentSrc.startsWith('https:')) {
115
+ return 'remote';
116
+ }
117
+ return 'unknown';
118
+ }
119
+
120
+ /**
121
+ * Builds a descriptive `secondaryError` for an `<img>` onerror so the downstream
122
+ * `loadFailed` analytics event reports a meaningful `error`/`errorDetail` instead of
123
+ * the opaque `unknown`/`unknown`. The returned Error's `message` captures decode
124
+ * diagnostics as a comma-separated `key=value` string (undefined fields are omitted):
125
+ * - whether the browser decoded any pixels (`naturalWidth`/`naturalHeight === 0`)
126
+ * - the file MIME type and whether it is natively browser-decodable
127
+ * - which source failed (preview blob vs. original-binary/HD url)
128
+ *
129
+ * @example
130
+ * // message:
131
+ * "mimeType=image/png, isBrowserDecodable=true, naturalWidth=0, naturalHeight=0, failedSrcType=blob"
132
+ */
133
+ function buildImgErrorDiagnostics(item, event) {
134
+ var img = event === null || event === void 0 ? void 0 : event.currentTarget;
135
+ var mimeType = item && !(0, _mediaClient.isErrorFileState)(item) ? item.mimeType : undefined;
136
+ var isBrowserDecodable = !!mimeType && (0, _isMimeTypeSupportedByBrowser.isImageMimeTypeSupportedByBrowser)(mimeType);
137
+ var naturalWidth = img === null || img === void 0 ? void 0 : img.naturalWidth;
138
+ var naturalHeight = img === null || img === void 0 ? void 0 : img.naturalHeight;
139
+ var failedSrcType = classifyFailedSrc(img === null || img === void 0 ? void 0 : img.currentSrc);
140
+ var diagnostics = {
141
+ mimeType: mimeType,
142
+ isBrowserDecodable: isBrowserDecodable,
143
+ naturalWidth: naturalWidth,
144
+ naturalHeight: naturalHeight,
145
+ failedSrcType: failedSrcType
146
+ };
147
+ var detail = Object.entries(diagnostics).filter(function (_ref3) {
148
+ var _ref4 = (0, _slicedToArray2.default)(_ref3, 2),
149
+ value = _ref4[1];
150
+ return value !== undefined;
151
+ }).map(function (_ref5) {
152
+ var _ref6 = (0, _slicedToArray2.default)(_ref5, 2),
153
+ key = _ref6[0],
154
+ value = _ref6[1];
155
+ return "".concat(key, "=").concat(value);
156
+ }).join(', ');
157
+ return new Error(detail);
94
158
  }
@@ -205,7 +205,8 @@ var Header = exports.Header = function Header(_ref) {
205
205
  state: item,
206
206
  identifier: identifier,
207
207
  mediaClient: mediaClient,
208
- traceContext: traceContext
208
+ traceContext: traceContext,
209
+ fallbackMediaName: fallbackMediaName
209
210
  });
210
211
  }
211
212
  })));
@@ -17,6 +17,7 @@ var _react = _interopRequireDefault(require("react"));
17
17
  var _mediaClient = require("@atlaskit/media-client");
18
18
  var _mediaCommon = require("@atlaskit/media-common");
19
19
  var _mediaUi = require("@atlaskit/media-ui");
20
+ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
20
21
  var _domain = require("../../domain");
21
22
  var _errors = require("../../errors");
22
23
  var _interactiveImg = require("./interactive-img");
@@ -43,8 +44,14 @@ var ImageViewer = exports.ImageViewer = /*#__PURE__*/function (_BaseViewer) {
43
44
  _this.props.onLoad();
44
45
  _this.onMediaDisplayed();
45
46
  });
46
- (0, _defineProperty2.default)(_this, "onImgError", function () {
47
- _this.props.onError(new _errors.MediaViewerError('imageviewer-src-onerror'));
47
+ (0, _defineProperty2.default)(_this, "onImgError", function (event) {
48
+ var secondaryError;
49
+ try {
50
+ secondaryError = (0, _errors.buildImgErrorDiagnostics)(_this.props.item, event);
51
+ } catch (_unused) {
52
+ secondaryError = undefined;
53
+ }
54
+ _this.props.onError(new _errors.MediaViewerError('imageviewer-src-onerror', secondaryError));
48
55
  });
49
56
  return _this;
50
57
  }
@@ -60,7 +67,7 @@ var ImageViewer = exports.ImageViewer = /*#__PURE__*/function (_BaseViewer) {
60
67
  key: "init",
61
68
  value: function () {
62
69
  var _init = (0, _asyncToGenerator2.default)( /*#__PURE__*/_regenerator.default.mark(function _callee() {
63
- var _this$props, fileState, mediaClient, collectionName, traceContext, controller, orientation, objectUrl, originalBinaryImageUrl, isLocalFileReference, clientId, preview, _yield$preview, value, origin, item, response, imgError, _t, _t2, _t3;
70
+ var _this$props, fileState, mediaClient, collectionName, traceContext, controller, orientation, objectUrl, originalBinaryImageUrl, isLocalFileReference, clientId, preview, _yield$preview, value, origin, item, response, unsupportedError, imgError, _t, _t2, _t3;
64
71
  return _regenerator.default.wrap(function (_context) {
65
72
  while (1) switch (_context.prev = _context.next) {
66
73
  case 0:
@@ -142,7 +149,11 @@ var ImageViewer = exports.ImageViewer = /*#__PURE__*/function (_BaseViewer) {
142
149
  case 14:
143
150
  if (!(!isLocalFileReference &&
144
151
  // objectUrl at this point is binary file already
145
- !(0, _mediaClient.isErrorFileState)(fileState) && fileState.status !== 'uploading' && fileState.mediaType === 'image' && (0, _mediaCommon.isImageMimeTypeSupportedByBrowser)(fileState.mimeType))) {
152
+ !(0, _mediaClient.isErrorFileState)(fileState) && fileState.status !== 'uploading' && fileState.mediaType === 'image')) {
153
+ _context.next = 17;
154
+ break;
155
+ }
156
+ if (!(0, _mediaCommon.isImageMimeTypeSupportedByBrowser)(fileState.mimeType)) {
146
157
  _context.next = 16;
147
158
  break;
148
159
  }
@@ -150,7 +161,25 @@ var ImageViewer = exports.ImageViewer = /*#__PURE__*/function (_BaseViewer) {
150
161
  return mediaClient.file.getFileBinaryURL(fileState.id, collectionName);
151
162
  case 15:
152
163
  originalBinaryImageUrl = _context.sent;
164
+ _context.next = 17;
165
+ break;
153
166
  case 16:
167
+ if (!(0, _platformFeatureFlags.fg)('platform_media_unsupported_mime_routing')) {
168
+ _context.next = 17;
169
+ break;
170
+ }
171
+ // Route unsupported MIME types browser can't natively decode straight to the
172
+ // unsupported/download view instead of handing a guaranteed-undecodable blob
173
+ // to <img>, which would only ever fire onerror and
174
+ // surface as an opaque `imageviewer-src-onerror` failure.
175
+ this.revokeObjectUrl(objectUrl);
176
+ unsupportedError = new _errors.MediaViewerError('imageviewer-unsupported-mime');
177
+ this.setState({
178
+ content: _domain.Outcome.failed(unsupportedError)
179
+ });
180
+ this.props.onError(unsupportedError);
181
+ return _context.abrupt("return");
182
+ case 17:
154
183
  this.setState({
155
184
  content: _domain.Outcome.successful({
156
185
  objectUrl: objectUrl,
@@ -159,10 +188,10 @@ var ImageViewer = exports.ImageViewer = /*#__PURE__*/function (_BaseViewer) {
159
188
  clientId: clientId
160
189
  })
161
190
  });
162
- _context.next = 18;
191
+ _context.next = 19;
163
192
  break;
164
- case 17:
165
- _context.prev = 17;
193
+ case 18:
194
+ _context.prev = 18;
166
195
  _t3 = _context["catch"](2);
167
196
  // TODO : properly handle aborted requests (MS-2843)
168
197
  if (!(controller !== null && controller !== void 0 && controller.signal.aborted)) {
@@ -172,11 +201,11 @@ var ImageViewer = exports.ImageViewer = /*#__PURE__*/function (_BaseViewer) {
172
201
  });
173
202
  this.props.onError(imgError);
174
203
  }
175
- case 18:
204
+ case 19:
176
205
  case "end":
177
206
  return _context.stop();
178
207
  }
179
- }, _callee, this, [[2, 17], [3, 5]]);
208
+ }, _callee, this, [[2, 18], [3, 5]]);
180
209
  }));
181
210
  function init() {
182
211
  return _init.apply(this, arguments);
@@ -1,7 +1,7 @@
1
1
  import { ANALYTICS_MEDIA_CHANNEL, sanitiseAnalyticsPayload } from '@atlaskit/media-common/analytics';
2
2
  const componentName = 'mediaViewer';
3
3
  const packageName = "@atlaskit/media-viewer";
4
- const packageVersion = "54.3.6";
4
+ const packageVersion = "54.4.0";
5
5
  export { packageName, packageVersion, componentName, componentName as component };
6
6
  export function getFileAttributes(fileState) {
7
7
  if (!fileState) {
@@ -3,7 +3,7 @@ import { getMediaEnvironment, getMediaRegion } from '@atlaskit/media-client';
3
3
  import { getFeatureFlagKeysAllProducts } from '@atlaskit/media-common';
4
4
  import { fg } from '@atlaskit/platform-feature-flags';
5
5
  const packageName = "@atlaskit/media-viewer";
6
- const packageVersion = "54.3.6";
6
+ const packageVersion = "54.4.0";
7
7
  let ufoExperience;
8
8
  const getExperience = () => {
9
9
  if (!ufoExperience) {
@@ -10,6 +10,7 @@ import { createDownloadFailedEventPayload, createDownloadSucceededEventPayload }
10
10
  import { fireAnalytics } from './analytics';
11
11
  import { createFailedPreviewDownloadButtonClickedEvent } from './analytics/events/ui/failedPreviewDownloadButtonClicked';
12
12
  import { DownloadButtonWrapper } from './styleWrappers';
13
+ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
13
14
  import { MediaViewerError } from './errors';
14
15
  import Tooltip from '@atlaskit/tooltip';
15
16
  import { AbuseModal } from '@atlaskit/media-ui/abuseModal';
@@ -45,10 +46,12 @@ const createItemDownloader = (file, mediaClient, options) => async () => {
45
46
  const {
46
47
  collectionName,
47
48
  traceContext,
48
- createAnalyticsEvent
49
+ createAnalyticsEvent,
50
+ fallbackMediaName
49
51
  } = options;
50
52
  const id = file.id;
51
- const name = !isErrorFileState(file) ? file.name : undefined;
53
+ const fileStateName = !isErrorFileState(file) ? file.name : undefined;
54
+ const name = fileStateName || (expValEquals('platform_editor_media_download_fallback_name', 'isEnabled', true) ? fallbackMediaName : undefined);
52
55
  mediaClient.file.downloadBinary(id, name, collectionName, traceContext).then(() => {
53
56
  fireAnalytics(createDownloadSucceededEventPayload(file, traceContext), createAnalyticsEvent);
54
57
  }).catch(e => {
@@ -75,7 +78,8 @@ const DownloadItem = ({
75
78
  analyticspayload,
76
79
  traceContext,
77
80
  iconBefore,
78
- children
81
+ children,
82
+ fallbackMediaName
79
83
  }) => {
80
84
  const [isAbuseModalOpen, setIsAbuseModalOpen] = useState(false);
81
85
  const {
@@ -89,7 +93,8 @@ const DownloadItem = ({
89
93
  const itemDownloader = createItemDownloader(fileState, mediaClient, {
90
94
  collectionName: collectionName,
91
95
  createAnalyticsEvent,
92
- traceContext
96
+ traceContext,
97
+ fallbackMediaName
93
98
  });
94
99
  return /*#__PURE__*/React.createElement(React.Fragment, null, shouldRenderAbuseModal && /*#__PURE__*/React.createElement(AbuseModal, {
95
100
  isOpen: isAbuseModalOpen,
@@ -134,7 +139,8 @@ export const ToolbarDownloadButton = ({
134
139
  state,
135
140
  mediaClient,
136
141
  identifier,
137
- traceContext
142
+ traceContext,
143
+ fallbackMediaName
138
144
  }) => {
139
145
  // TODO [MS-1731]: make it work for external files as well
140
146
  if (isExternalImageIdentifier(identifier)) {
@@ -149,7 +155,8 @@ export const ToolbarDownloadButton = ({
149
155
  mediaClient: mediaClient,
150
156
  collectionName: identifier.collectionName,
151
157
  traceContext: traceContext,
152
- iconBefore: downloadIcon
158
+ iconBefore: downloadIcon,
159
+ fallbackMediaName: fallbackMediaName
153
160
  });
154
161
  };
155
162
  export const DisabledToolbarDownloadButton = /*#__PURE__*/React.createElement(MediaButton, {
@@ -1,3 +1,4 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
1
2
  import React from 'react';
2
3
  import { FormattedMessage, injectIntl } from 'react-intl';
3
4
  import { messages as i18nMessages } from '@atlaskit/media-ui';
@@ -26,11 +27,12 @@ export class ErrorMessage extends React.Component {
26
27
  },
27
28
  error
28
29
  } = this.props;
29
- // Non-ZIP archives (e.g. RAR, TAR, 7z) aren't a load failure - the
30
+ // Non-ZIP archives (e.g. RAR, TAR, 7z) and image MIME types that no browser
31
+ // can natively decode (e.g. HEIC/HEIF, PSD, TIFF) aren't load failures - the
30
32
  // browser-based viewer simply can't preview them. Show a dedicated
31
33
  // "Unsupported file format" heading instead of the generic
32
34
  // "Something went wrong" copy.
33
- if (getPrimaryErrorReason(error) === 'archiveviewer-not-zip') {
35
+ if (getPrimaryErrorReason(error) === 'archiveviewer-not-zip' || getPrimaryErrorReason(error) === 'imageviewer-unsupported-mime') {
34
36
  return {
35
37
  icon: errorLoadingFileImage(formatMessage),
36
38
  messages: [i18nMessages.unsupported_file_format, i18nMessages.archive_format_not_supported]
@@ -73,9 +75,18 @@ export class ErrorMessage extends React.Component {
73
75
  failMediaFileUfoExperience(failMediaFileUfoExperiencePayload);
74
76
  }
75
77
  }
78
+
79
+ // Error reasons that represent a format the browser-based viewer simply can't
80
+ // preview (not a load failure). These are reported as the informational,
81
+ // non-SLI `previewUnsupported` metric instead of `loadFailed`.
82
+
83
+ static isPreviewUnsupported(error) {
84
+ return ErrorMessage.previewUnsupportedReasons.includes(getPrimaryErrorReason(error));
85
+ }
76
86
  static getEventPayload(error, fileId, fileState, traceContext) {
77
- if (fileState && getPrimaryErrorReason(error) === 'unsupported') {
78
- // this is not an SLI, its just a useful metric for unsupported
87
+ if (fileState && ErrorMessage.isPreviewUnsupported(error)) {
88
+ // this is not an SLI (load failure), its just a useful metric for files
89
+ // whose format the browser-based viewer can't preview.
79
90
  return createPreviewUnsupportedEvent(fileState);
80
91
  } else {
81
92
  return createLoadFailedEvent(fileId, error, fileState, traceContext);
@@ -95,5 +106,6 @@ export class ErrorMessage extends React.Component {
95
106
  }
96
107
 
97
108
  // @ts-ignore: [PIT-1685] Fails in post-office due to backwards incompatibility issue with React 18
109
+ _defineProperty(ErrorMessage, "previewUnsupportedReasons", ['unsupported', 'imageviewer-unsupported-mime']);
98
110
  const ErroMsg = withAnalyticsEvents()(injectIntl(ErrorMessage));
99
111
  export default ErroMsg;
@@ -1,4 +1,5 @@
1
- import { isCommonMediaClientError } from '@atlaskit/media-client';
1
+ import { isCommonMediaClientError, isErrorFileState } from '@atlaskit/media-client';
2
+ import { isImageMimeTypeSupportedByBrowser } from '@atlaskit/media-common/isMimeTypeSupportedByBrowser';
2
3
  export class MediaViewerError extends Error {
3
4
  constructor(primaryReason, secondaryError) {
4
5
  super(primaryReason);
@@ -64,4 +65,55 @@ export function getRequestMetadata(error) {
64
65
  if (isCommonMediaClientError(secondaryError)) {
65
66
  return secondaryError.metadata;
66
67
  }
68
+ }
69
+
70
+ /**
71
+ * Classifies the failed `<img>` source into a coarse, non-sensitive enum so we can
72
+ * tell which kind of source failed without leaking the (potentially signed) url.
73
+ */
74
+ export function classifyFailedSrc(currentSrc) {
75
+ if (!currentSrc) {
76
+ return 'unknown';
77
+ }
78
+ if (currentSrc.startsWith('blob:')) {
79
+ return 'blob';
80
+ }
81
+ if (currentSrc.startsWith('data:')) {
82
+ return 'data';
83
+ }
84
+ if (currentSrc.startsWith('http:') || currentSrc.startsWith('https:')) {
85
+ return 'remote';
86
+ }
87
+ return 'unknown';
88
+ }
89
+
90
+ /**
91
+ * Builds a descriptive `secondaryError` for an `<img>` onerror so the downstream
92
+ * `loadFailed` analytics event reports a meaningful `error`/`errorDetail` instead of
93
+ * the opaque `unknown`/`unknown`. The returned Error's `message` captures decode
94
+ * diagnostics as a comma-separated `key=value` string (undefined fields are omitted):
95
+ * - whether the browser decoded any pixels (`naturalWidth`/`naturalHeight === 0`)
96
+ * - the file MIME type and whether it is natively browser-decodable
97
+ * - which source failed (preview blob vs. original-binary/HD url)
98
+ *
99
+ * @example
100
+ * // message:
101
+ * "mimeType=image/png, isBrowserDecodable=true, naturalWidth=0, naturalHeight=0, failedSrcType=blob"
102
+ */
103
+ export function buildImgErrorDiagnostics(item, event) {
104
+ const img = event === null || event === void 0 ? void 0 : event.currentTarget;
105
+ const mimeType = item && !isErrorFileState(item) ? item.mimeType : undefined;
106
+ const isBrowserDecodable = !!mimeType && isImageMimeTypeSupportedByBrowser(mimeType);
107
+ const naturalWidth = img === null || img === void 0 ? void 0 : img.naturalWidth;
108
+ const naturalHeight = img === null || img === void 0 ? void 0 : img.naturalHeight;
109
+ const failedSrcType = classifyFailedSrc(img === null || img === void 0 ? void 0 : img.currentSrc);
110
+ const diagnostics = {
111
+ mimeType,
112
+ isBrowserDecodable,
113
+ naturalWidth,
114
+ naturalHeight,
115
+ failedSrcType
116
+ };
117
+ const detail = Object.entries(diagnostics).filter(([, value]) => value !== undefined).map(([key, value]) => `${key}=${value}`).join(', ');
118
+ return new Error(detail);
67
119
  }
@@ -181,7 +181,8 @@ export const Header = ({
181
181
  state: item,
182
182
  identifier: identifier,
183
183
  mediaClient: mediaClient,
184
- traceContext: traceContext
184
+ traceContext: traceContext,
185
+ fallbackMediaName: fallbackMediaName
185
186
  })
186
187
  })));
187
188
  };
@@ -3,8 +3,9 @@ import React from 'react';
3
3
  import { isImageRepresentationReady, isErrorFileState, addFileAttrsToUrl } from '@atlaskit/media-client';
4
4
  import { isImageMimeTypeSupportedByBrowser } from '@atlaskit/media-common';
5
5
  import { getOrientation } from '@atlaskit/media-ui';
6
+ import { fg } from '@atlaskit/platform-feature-flags';
6
7
  import { Outcome } from '../../domain';
7
- import { MediaViewerError } from '../../errors';
8
+ import { buildImgErrorDiagnostics, MediaViewerError } from '../../errors';
8
9
  import { InteractiveImg } from './interactive-img';
9
10
  import { BaseViewer } from '../base-viewer';
10
11
  function processedFileStateToMediaItem(file) {
@@ -22,8 +23,14 @@ export class ImageViewer extends BaseViewer {
22
23
  this.props.onLoad();
23
24
  this.onMediaDisplayed();
24
25
  });
25
- _defineProperty(this, "onImgError", () => {
26
- this.props.onError(new MediaViewerError('imageviewer-src-onerror'));
26
+ _defineProperty(this, "onImgError", event => {
27
+ let secondaryError;
28
+ try {
29
+ secondaryError = buildImgErrorDiagnostics(this.props.item, event);
30
+ } catch {
31
+ secondaryError = undefined;
32
+ }
33
+ this.props.onError(new MediaViewerError('imageviewer-src-onerror', secondaryError));
27
34
  });
28
35
  }
29
36
  get initialState() {
@@ -86,8 +93,22 @@ export class ImageViewer extends BaseViewer {
86
93
  }
87
94
  if (!isLocalFileReference &&
88
95
  // objectUrl at this point is binary file already
89
- !isErrorFileState(fileState) && fileState.status !== 'uploading' && fileState.mediaType === 'image' && isImageMimeTypeSupportedByBrowser(fileState.mimeType)) {
90
- originalBinaryImageUrl = await mediaClient.file.getFileBinaryURL(fileState.id, collectionName);
96
+ !isErrorFileState(fileState) && fileState.status !== 'uploading' && fileState.mediaType === 'image') {
97
+ if (isImageMimeTypeSupportedByBrowser(fileState.mimeType)) {
98
+ originalBinaryImageUrl = await mediaClient.file.getFileBinaryURL(fileState.id, collectionName);
99
+ } else if (fg('platform_media_unsupported_mime_routing')) {
100
+ // Route unsupported MIME types browser can't natively decode straight to the
101
+ // unsupported/download view instead of handing a guaranteed-undecodable blob
102
+ // to <img>, which would only ever fire onerror and
103
+ // surface as an opaque `imageviewer-src-onerror` failure.
104
+ this.revokeObjectUrl(objectUrl);
105
+ const unsupportedError = new MediaViewerError('imageviewer-unsupported-mime');
106
+ this.setState({
107
+ content: Outcome.failed(unsupportedError)
108
+ });
109
+ this.props.onError(unsupportedError);
110
+ return;
111
+ }
91
112
  }
92
113
  this.setState({
93
114
  content: Outcome.successful({
@@ -1,7 +1,7 @@
1
1
  import { ANALYTICS_MEDIA_CHANNEL, sanitiseAnalyticsPayload } from '@atlaskit/media-common/analytics';
2
2
  var componentName = 'mediaViewer';
3
3
  var packageName = "@atlaskit/media-viewer";
4
- var packageVersion = "54.3.6";
4
+ var packageVersion = "54.4.0";
5
5
  export { packageName, packageVersion, componentName, componentName as component };
6
6
  export function getFileAttributes(fileState) {
7
7
  if (!fileState) {
@@ -6,7 +6,7 @@ import { getMediaEnvironment, getMediaRegion } from '@atlaskit/media-client';
6
6
  import { getFeatureFlagKeysAllProducts } from '@atlaskit/media-common';
7
7
  import { fg } from '@atlaskit/platform-feature-flags';
8
8
  var packageName = "@atlaskit/media-viewer";
9
- var packageVersion = "54.3.6";
9
+ var packageVersion = "54.4.0";
10
10
  var ufoExperience;
11
11
  var getExperience = function getExperience() {
12
12
  if (!ufoExperience) {
@@ -15,6 +15,7 @@ import { createDownloadFailedEventPayload, createDownloadSucceededEventPayload }
15
15
  import { fireAnalytics } from './analytics';
16
16
  import { createFailedPreviewDownloadButtonClickedEvent } from './analytics/events/ui/failedPreviewDownloadButtonClicked';
17
17
  import { DownloadButtonWrapper } from './styleWrappers';
18
+ import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
18
19
  import { MediaViewerError } from './errors';
19
20
  import Tooltip from '@atlaskit/tooltip';
20
21
  import { AbuseModal } from '@atlaskit/media-ui/abuseModal';
@@ -47,13 +48,14 @@ function DownloadButton(_ref) {
47
48
  }
48
49
  var createItemDownloader = function createItemDownloader(file, mediaClient, options) {
49
50
  return /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee() {
50
- var collectionName, traceContext, createAnalyticsEvent, id, name;
51
+ var collectionName, traceContext, createAnalyticsEvent, fallbackMediaName, id, fileStateName, name;
51
52
  return _regeneratorRuntime.wrap(function (_context) {
52
53
  while (1) switch (_context.prev = _context.next) {
53
54
  case 0:
54
- collectionName = options.collectionName, traceContext = options.traceContext, createAnalyticsEvent = options.createAnalyticsEvent;
55
+ collectionName = options.collectionName, traceContext = options.traceContext, createAnalyticsEvent = options.createAnalyticsEvent, fallbackMediaName = options.fallbackMediaName;
55
56
  id = file.id;
56
- name = !isErrorFileState(file) ? file.name : undefined;
57
+ fileStateName = !isErrorFileState(file) ? file.name : undefined;
58
+ name = fileStateName || (expValEquals('platform_editor_media_download_fallback_name', 'isEnabled', true) ? fallbackMediaName : undefined);
57
59
  mediaClient.file.downloadBinary(id, name, collectionName, traceContext).then(function () {
58
60
  fireAnalytics(createDownloadSucceededEventPayload(file, traceContext), createAnalyticsEvent);
59
61
  }).catch(function (e) {
@@ -85,7 +87,8 @@ var DownloadItem = function DownloadItem(_ref3) {
85
87
  analyticspayload = _ref3.analyticspayload,
86
88
  traceContext = _ref3.traceContext,
87
89
  iconBefore = _ref3.iconBefore,
88
- children = _ref3.children;
90
+ children = _ref3.children,
91
+ fallbackMediaName = _ref3.fallbackMediaName;
89
92
  var _useState = useState(false),
90
93
  _useState2 = _slicedToArray(_useState, 2),
91
94
  isAbuseModalOpen = _useState2[0],
@@ -99,7 +102,8 @@ var DownloadItem = function DownloadItem(_ref3) {
99
102
  var itemDownloader = createItemDownloader(fileState, mediaClient, {
100
103
  collectionName: collectionName,
101
104
  createAnalyticsEvent: createAnalyticsEvent,
102
- traceContext: traceContext
105
+ traceContext: traceContext,
106
+ fallbackMediaName: fallbackMediaName
103
107
  });
104
108
  return /*#__PURE__*/React.createElement(React.Fragment, null, shouldRenderAbuseModal && /*#__PURE__*/React.createElement(AbuseModal, {
105
109
  isOpen: isAbuseModalOpen,
@@ -145,7 +149,8 @@ export var ToolbarDownloadButton = function ToolbarDownloadButton(_ref5) {
145
149
  var state = _ref5.state,
146
150
  mediaClient = _ref5.mediaClient,
147
151
  identifier = _ref5.identifier,
148
- traceContext = _ref5.traceContext;
152
+ traceContext = _ref5.traceContext,
153
+ fallbackMediaName = _ref5.fallbackMediaName;
149
154
  // TODO [MS-1731]: make it work for external files as well
150
155
  if (isExternalImageIdentifier(identifier)) {
151
156
  return null;
@@ -159,7 +164,8 @@ export var ToolbarDownloadButton = function ToolbarDownloadButton(_ref5) {
159
164
  mediaClient: mediaClient,
160
165
  collectionName: identifier.collectionName,
161
166
  traceContext: traceContext,
162
- iconBefore: downloadIcon
167
+ iconBefore: downloadIcon,
168
+ fallbackMediaName: fallbackMediaName
163
169
  });
164
170
  };
165
171
  export var DisabledToolbarDownloadButton = /*#__PURE__*/React.createElement(MediaButton, {
@@ -1,9 +1,9 @@
1
- import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
1
  import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
3
2
  import _createClass from "@babel/runtime/helpers/createClass";
4
3
  import _possibleConstructorReturn from "@babel/runtime/helpers/possibleConstructorReturn";
5
4
  import _getPrototypeOf from "@babel/runtime/helpers/getPrototypeOf";
6
5
  import _inherits from "@babel/runtime/helpers/inherits";
6
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
7
7
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
8
8
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
9
9
  function _callSuper(t, o, e) { return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e)); }
@@ -44,11 +44,12 @@ export var ErrorMessage = /*#__PURE__*/function (_React$Component) {
44
44
  var _this$props = this.props,
45
45
  formatMessage = _this$props.intl.formatMessage,
46
46
  error = _this$props.error;
47
- // Non-ZIP archives (e.g. RAR, TAR, 7z) aren't a load failure - the
47
+ // Non-ZIP archives (e.g. RAR, TAR, 7z) and image MIME types that no browser
48
+ // can natively decode (e.g. HEIC/HEIF, PSD, TIFF) aren't load failures - the
48
49
  // browser-based viewer simply can't preview them. Show a dedicated
49
50
  // "Unsupported file format" heading instead of the generic
50
51
  // "Something went wrong" copy.
51
- if (getPrimaryErrorReason(error) === 'archiveviewer-not-zip') {
52
+ if (getPrimaryErrorReason(error) === 'archiveviewer-not-zip' || getPrimaryErrorReason(error) === 'imageviewer-unsupported-mime') {
52
53
  return {
53
54
  icon: errorLoadingFileImage(formatMessage),
54
55
  messages: [i18nMessages.unsupported_file_format, i18nMessages.archive_format_not_supported]
@@ -88,6 +89,10 @@ export var ErrorMessage = /*#__PURE__*/function (_React$Component) {
88
89
  failMediaFileUfoExperience(failMediaFileUfoExperiencePayload);
89
90
  }
90
91
  }
92
+
93
+ // Error reasons that represent a format the browser-based viewer simply can't
94
+ // preview (not a load failure). These are reported as the informational,
95
+ // non-SLI `previewUnsupported` metric instead of `loadFailed`.
91
96
  }, {
92
97
  key: "render",
93
98
  value: function render() {
@@ -105,10 +110,16 @@ export var ErrorMessage = /*#__PURE__*/function (_React$Component) {
105
110
  })), this.props.children);
106
111
  }
107
112
  }], [{
113
+ key: "isPreviewUnsupported",
114
+ value: function isPreviewUnsupported(error) {
115
+ return ErrorMessage.previewUnsupportedReasons.includes(getPrimaryErrorReason(error));
116
+ }
117
+ }, {
108
118
  key: "getEventPayload",
109
119
  value: function getEventPayload(error, fileId, fileState, traceContext) {
110
- if (fileState && getPrimaryErrorReason(error) === 'unsupported') {
111
- // this is not an SLI, its just a useful metric for unsupported
120
+ if (fileState && ErrorMessage.isPreviewUnsupported(error)) {
121
+ // this is not an SLI (load failure), its just a useful metric for files
122
+ // whose format the browser-based viewer can't preview.
112
123
  return createPreviewUnsupportedEvent(fileState);
113
124
  } else {
114
125
  return createLoadFailedEvent(fileId, error, fileState, traceContext);
@@ -118,5 +129,6 @@ export var ErrorMessage = /*#__PURE__*/function (_React$Component) {
118
129
  }(React.Component);
119
130
 
120
131
  // @ts-ignore: [PIT-1685] Fails in post-office due to backwards incompatibility issue with React 18
132
+ _defineProperty(ErrorMessage, "previewUnsupportedReasons", ['unsupported', 'imageviewer-unsupported-mime']);
121
133
  var ErroMsg = withAnalyticsEvents()(injectIntl(ErrorMessage));
122
134
  export default ErroMsg;
@@ -1,3 +1,4 @@
1
+ import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
1
2
  import _createClass from "@babel/runtime/helpers/createClass";
2
3
  import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
3
4
  import _possibleConstructorReturn from "@babel/runtime/helpers/possibleConstructorReturn";
@@ -6,7 +7,8 @@ import _inherits from "@babel/runtime/helpers/inherits";
6
7
  import _wrapNativeSuper from "@babel/runtime/helpers/wrapNativeSuper";
7
8
  function _callSuper(t, o, e) { return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e)); }
8
9
  function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); }
9
- import { isCommonMediaClientError } from '@atlaskit/media-client';
10
+ import { isCommonMediaClientError, isErrorFileState } from '@atlaskit/media-client';
11
+ import { isImageMimeTypeSupportedByBrowser } from '@atlaskit/media-common/isMimeTypeSupportedByBrowser';
10
12
  export var MediaViewerError = /*#__PURE__*/function (_Error) {
11
13
  function MediaViewerError(primaryReason, secondaryError) {
12
14
  var _this;
@@ -78,4 +80,64 @@ export function getRequestMetadata(error) {
78
80
  if (isCommonMediaClientError(secondaryError)) {
79
81
  return secondaryError.metadata;
80
82
  }
83
+ }
84
+
85
+ /**
86
+ * Classifies the failed `<img>` source into a coarse, non-sensitive enum so we can
87
+ * tell which kind of source failed without leaking the (potentially signed) url.
88
+ */
89
+ export function classifyFailedSrc(currentSrc) {
90
+ if (!currentSrc) {
91
+ return 'unknown';
92
+ }
93
+ if (currentSrc.startsWith('blob:')) {
94
+ return 'blob';
95
+ }
96
+ if (currentSrc.startsWith('data:')) {
97
+ return 'data';
98
+ }
99
+ if (currentSrc.startsWith('http:') || currentSrc.startsWith('https:')) {
100
+ return 'remote';
101
+ }
102
+ return 'unknown';
103
+ }
104
+
105
+ /**
106
+ * Builds a descriptive `secondaryError` for an `<img>` onerror so the downstream
107
+ * `loadFailed` analytics event reports a meaningful `error`/`errorDetail` instead of
108
+ * the opaque `unknown`/`unknown`. The returned Error's `message` captures decode
109
+ * diagnostics as a comma-separated `key=value` string (undefined fields are omitted):
110
+ * - whether the browser decoded any pixels (`naturalWidth`/`naturalHeight === 0`)
111
+ * - the file MIME type and whether it is natively browser-decodable
112
+ * - which source failed (preview blob vs. original-binary/HD url)
113
+ *
114
+ * @example
115
+ * // message:
116
+ * "mimeType=image/png, isBrowserDecodable=true, naturalWidth=0, naturalHeight=0, failedSrcType=blob"
117
+ */
118
+ export function buildImgErrorDiagnostics(item, event) {
119
+ var img = event === null || event === void 0 ? void 0 : event.currentTarget;
120
+ var mimeType = item && !isErrorFileState(item) ? item.mimeType : undefined;
121
+ var isBrowserDecodable = !!mimeType && isImageMimeTypeSupportedByBrowser(mimeType);
122
+ var naturalWidth = img === null || img === void 0 ? void 0 : img.naturalWidth;
123
+ var naturalHeight = img === null || img === void 0 ? void 0 : img.naturalHeight;
124
+ var failedSrcType = classifyFailedSrc(img === null || img === void 0 ? void 0 : img.currentSrc);
125
+ var diagnostics = {
126
+ mimeType: mimeType,
127
+ isBrowserDecodable: isBrowserDecodable,
128
+ naturalWidth: naturalWidth,
129
+ naturalHeight: naturalHeight,
130
+ failedSrcType: failedSrcType
131
+ };
132
+ var detail = Object.entries(diagnostics).filter(function (_ref3) {
133
+ var _ref4 = _slicedToArray(_ref3, 2),
134
+ value = _ref4[1];
135
+ return value !== undefined;
136
+ }).map(function (_ref5) {
137
+ var _ref6 = _slicedToArray(_ref5, 2),
138
+ key = _ref6[0],
139
+ value = _ref6[1];
140
+ return "".concat(key, "=").concat(value);
141
+ }).join(', ');
142
+ return new Error(detail);
81
143
  }
@@ -196,7 +196,8 @@ export var Header = function Header(_ref) {
196
196
  state: item,
197
197
  identifier: identifier,
198
198
  mediaClient: mediaClient,
199
- traceContext: traceContext
199
+ traceContext: traceContext,
200
+ fallbackMediaName: fallbackMediaName
200
201
  });
201
202
  }
202
203
  })));
@@ -12,8 +12,9 @@ import React from 'react';
12
12
  import { isImageRepresentationReady, isErrorFileState, addFileAttrsToUrl } from '@atlaskit/media-client';
13
13
  import { isImageMimeTypeSupportedByBrowser } from '@atlaskit/media-common';
14
14
  import { getOrientation } from '@atlaskit/media-ui';
15
+ import { fg } from '@atlaskit/platform-feature-flags';
15
16
  import { Outcome } from '../../domain';
16
- import { MediaViewerError } from '../../errors';
17
+ import { buildImgErrorDiagnostics, MediaViewerError } from '../../errors';
17
18
  import { InteractiveImg } from './interactive-img';
18
19
  import { BaseViewer } from '../base-viewer';
19
20
  function processedFileStateToMediaItem(file) {
@@ -36,8 +37,14 @@ export var ImageViewer = /*#__PURE__*/function (_BaseViewer) {
36
37
  _this.props.onLoad();
37
38
  _this.onMediaDisplayed();
38
39
  });
39
- _defineProperty(_this, "onImgError", function () {
40
- _this.props.onError(new MediaViewerError('imageviewer-src-onerror'));
40
+ _defineProperty(_this, "onImgError", function (event) {
41
+ var secondaryError;
42
+ try {
43
+ secondaryError = buildImgErrorDiagnostics(_this.props.item, event);
44
+ } catch (_unused) {
45
+ secondaryError = undefined;
46
+ }
47
+ _this.props.onError(new MediaViewerError('imageviewer-src-onerror', secondaryError));
41
48
  });
42
49
  return _this;
43
50
  }
@@ -53,7 +60,7 @@ export var ImageViewer = /*#__PURE__*/function (_BaseViewer) {
53
60
  key: "init",
54
61
  value: function () {
55
62
  var _init = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee() {
56
- var _this$props, fileState, mediaClient, collectionName, traceContext, controller, orientation, objectUrl, originalBinaryImageUrl, isLocalFileReference, clientId, preview, _yield$preview, value, origin, item, response, imgError, _t, _t2, _t3;
63
+ var _this$props, fileState, mediaClient, collectionName, traceContext, controller, orientation, objectUrl, originalBinaryImageUrl, isLocalFileReference, clientId, preview, _yield$preview, value, origin, item, response, unsupportedError, imgError, _t, _t2, _t3;
57
64
  return _regeneratorRuntime.wrap(function (_context) {
58
65
  while (1) switch (_context.prev = _context.next) {
59
66
  case 0:
@@ -135,7 +142,11 @@ export var ImageViewer = /*#__PURE__*/function (_BaseViewer) {
135
142
  case 14:
136
143
  if (!(!isLocalFileReference &&
137
144
  // objectUrl at this point is binary file already
138
- !isErrorFileState(fileState) && fileState.status !== 'uploading' && fileState.mediaType === 'image' && isImageMimeTypeSupportedByBrowser(fileState.mimeType))) {
145
+ !isErrorFileState(fileState) && fileState.status !== 'uploading' && fileState.mediaType === 'image')) {
146
+ _context.next = 17;
147
+ break;
148
+ }
149
+ if (!isImageMimeTypeSupportedByBrowser(fileState.mimeType)) {
139
150
  _context.next = 16;
140
151
  break;
141
152
  }
@@ -143,7 +154,25 @@ export var ImageViewer = /*#__PURE__*/function (_BaseViewer) {
143
154
  return mediaClient.file.getFileBinaryURL(fileState.id, collectionName);
144
155
  case 15:
145
156
  originalBinaryImageUrl = _context.sent;
157
+ _context.next = 17;
158
+ break;
146
159
  case 16:
160
+ if (!fg('platform_media_unsupported_mime_routing')) {
161
+ _context.next = 17;
162
+ break;
163
+ }
164
+ // Route unsupported MIME types browser can't natively decode straight to the
165
+ // unsupported/download view instead of handing a guaranteed-undecodable blob
166
+ // to <img>, which would only ever fire onerror and
167
+ // surface as an opaque `imageviewer-src-onerror` failure.
168
+ this.revokeObjectUrl(objectUrl);
169
+ unsupportedError = new MediaViewerError('imageviewer-unsupported-mime');
170
+ this.setState({
171
+ content: Outcome.failed(unsupportedError)
172
+ });
173
+ this.props.onError(unsupportedError);
174
+ return _context.abrupt("return");
175
+ case 17:
147
176
  this.setState({
148
177
  content: Outcome.successful({
149
178
  objectUrl: objectUrl,
@@ -152,10 +181,10 @@ export var ImageViewer = /*#__PURE__*/function (_BaseViewer) {
152
181
  clientId: clientId
153
182
  })
154
183
  });
155
- _context.next = 18;
184
+ _context.next = 19;
156
185
  break;
157
- case 17:
158
- _context.prev = 17;
186
+ case 18:
187
+ _context.prev = 18;
159
188
  _t3 = _context["catch"](2);
160
189
  // TODO : properly handle aborted requests (MS-2843)
161
190
  if (!(controller !== null && controller !== void 0 && controller.signal.aborted)) {
@@ -165,11 +194,11 @@ export var ImageViewer = /*#__PURE__*/function (_BaseViewer) {
165
194
  });
166
195
  this.props.onError(imgError);
167
196
  }
168
- case 18:
197
+ case 19:
169
198
  case "end":
170
199
  return _context.stop();
171
200
  }
172
- }, _callee, this, [[2, 17], [3, 5]]);
201
+ }, _callee, this, [[2, 18], [3, 5]]);
173
202
  }));
174
203
  function init() {
175
204
  return _init.apply(this, arguments);
@@ -15,6 +15,7 @@ export type ToolbarDownloadButtonProps = {
15
15
  identifier: Identifier;
16
16
  mediaClient: MediaClient;
17
17
  traceContext: MediaTraceContext;
18
+ fallbackMediaName?: string;
18
19
  };
19
- export declare const ToolbarDownloadButton: ({ state, mediaClient, identifier, traceContext, }: ToolbarDownloadButtonProps) => React.JSX.Element | null;
20
+ export declare const ToolbarDownloadButton: ({ state, mediaClient, identifier, traceContext, fallbackMediaName, }: ToolbarDownloadButtonProps) => React.JSX.Element | null;
20
21
  export declare const DisabledToolbarDownloadButton: React.JSX.Element;
@@ -26,6 +26,8 @@ export declare const getErrorMessageFromError: (error: MediaViewerError) => Mess
26
26
  export declare class ErrorMessage extends React.Component<Props & WrappedComponentProps & WithAnalyticsEventsProps, {}> {
27
27
  private getErrorInfo;
28
28
  componentDidMount(): void;
29
+ private static readonly previewUnsupportedReasons;
30
+ static isPreviewUnsupported(error: MediaViewerError): boolean;
29
31
  static getEventPayload(error: MediaViewerError, fileId: string, fileState?: FileState, traceContext?: MediaTraceContext): PreviewUnsupportedEventPayload | LoadFailedEventPayload;
30
32
  render(): React.JSX.Element;
31
33
  }
@@ -1,4 +1,5 @@
1
- import { type MediaClientErrorReason, type RequestErrorMetadata } from '@atlaskit/media-client';
1
+ import { type SyntheticEvent } from 'react';
2
+ import { type FileState, type MediaClientErrorReason, type RequestErrorMetadata } from '@atlaskit/media-client';
2
3
  import { type ZipEntry } from 'unzipit';
3
4
  export declare class MediaViewerError extends Error {
4
5
  readonly primaryReason: MediaViewerErrorReason | ArchiveViewerErrorReason;
@@ -13,7 +14,8 @@ export declare class ArchiveViewerError extends MediaViewerError {
13
14
  constructor(primaryReason: ArchiveViewerErrorReason, secondaryError?: Error | undefined, zipEntry?: ZipEntry | undefined);
14
15
  }
15
16
  export declare function isArchiveViewerError(err: Error): err is ArchiveViewerError;
16
- export type MediaViewerErrorReason = 'collection-fetch-metadata' | 'header-fetch-metadata' | 'itemviewer-onerror' | 'itemviewer-fetch-metadata' | 'itemviewer-file-error-status' | 'itemviewer-file-failed-processing-status' | 'imageviewer-external-onerror' | 'imageviewer-fetch-url' | 'imageviewer-src-onerror' | 'audioviewer-fetch-url' | 'audioviewer-missing-artefact' | 'audioviewer-playback' | 'videoviewer-fetch-url' | 'videoviewer-missing-artefact' | 'videoviewer-playback' | 'docviewer-fetch-url' | 'docviewer-content-fetch-failed' | 'docviewer-fetch-pdf' | 'codeviewer-fetch-src' | 'codeviewer-load-src' | 'codeviewer-file-size-exceeds' | 'codeviewer-parse-email' | 'svg-img-error' | 'svg-binary-fetch' | 'svg-unknown-error' | 'svg-blob-to-datauri' | 'unsupported' | 'custom-viewer-error' | 'download';
17
+ type srcType = 'blob' | 'remote' | 'data' | 'unknown';
18
+ export type MediaViewerErrorReason = 'collection-fetch-metadata' | 'header-fetch-metadata' | 'itemviewer-onerror' | 'itemviewer-fetch-metadata' | 'itemviewer-file-error-status' | 'itemviewer-file-failed-processing-status' | 'imageviewer-external-onerror' | 'imageviewer-fetch-url' | 'imageviewer-src-onerror' | 'imageviewer-unsupported-mime' | 'audioviewer-fetch-url' | 'audioviewer-missing-artefact' | 'audioviewer-playback' | 'videoviewer-fetch-url' | 'videoviewer-missing-artefact' | 'videoviewer-playback' | 'docviewer-fetch-url' | 'docviewer-content-fetch-failed' | 'docviewer-fetch-pdf' | 'codeviewer-fetch-src' | 'codeviewer-load-src' | 'codeviewer-file-size-exceeds' | 'codeviewer-parse-email' | 'svg-img-error' | 'svg-binary-fetch' | 'svg-unknown-error' | 'svg-blob-to-datauri' | 'unsupported' | 'custom-viewer-error' | 'download';
17
19
  export type ArchiveViewerErrorReason = 'archiveviewer-bundle-loader' | 'archiveviewer-read-binary' | 'archiveviewer-not-zip' | 'archiveviewer-create-url' | 'archiveviewer-imageviewer-onerror' | 'archiveviewer-videoviewer-onerror' | 'archiveviewer-audioviewer-onerror' | 'archiveviewer-docviewer-onerror' | 'archiveviewer-codeviewer-onerror' | 'archiveviewer-codeviewer-file-size-exceeds' | 'archiveviewer-missing-name-src' | 'archiveviewer-unsupported' | 'archiveviewer-encrypted-entry' | 'archiveviewer-customrenderer-onerror';
18
20
  export type PrimaryErrorReason = MediaViewerErrorReason | ArchiveViewerErrorReason;
19
21
  export type SecondaryErrorReason = MediaClientErrorReason | 'unknown' | 'nativeError' | undefined;
@@ -21,3 +23,23 @@ export declare function getPrimaryErrorReason(error: MediaViewerError): PrimaryE
21
23
  export declare function getSecondaryErrorReason(error: MediaViewerError): SecondaryErrorReason;
22
24
  export declare function getErrorDetail(error?: MediaViewerError): string;
23
25
  export declare function getRequestMetadata(error?: MediaViewerError): RequestErrorMetadata | undefined;
26
+ /**
27
+ * Classifies the failed `<img>` source into a coarse, non-sensitive enum so we can
28
+ * tell which kind of source failed without leaking the (potentially signed) url.
29
+ */
30
+ export declare function classifyFailedSrc(currentSrc?: string): srcType;
31
+ /**
32
+ * Builds a descriptive `secondaryError` for an `<img>` onerror so the downstream
33
+ * `loadFailed` analytics event reports a meaningful `error`/`errorDetail` instead of
34
+ * the opaque `unknown`/`unknown`. The returned Error's `message` captures decode
35
+ * diagnostics as a comma-separated `key=value` string (undefined fields are omitted):
36
+ * - whether the browser decoded any pixels (`naturalWidth`/`naturalHeight === 0`)
37
+ * - the file MIME type and whether it is natively browser-decodable
38
+ * - which source failed (preview blob vs. original-binary/HD url)
39
+ *
40
+ * @example
41
+ * // message:
42
+ * "mimeType=image/png, isBrowserDecodable=true, naturalWidth=0, naturalHeight=0, failedSrcType=blob"
43
+ */
44
+ export declare function buildImgErrorDiagnostics(item: FileState | undefined, event?: SyntheticEvent<HTMLImageElement, Event>): Error;
45
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/media-viewer",
3
- "version": "54.3.7",
3
+ "version": "54.4.0",
4
4
  "description": "MediaViewer is Atlassian's powerful solution for viewing files on the web. It's both powerful and extendable yet easy-to-integrate",
5
5
  "publishConfig": {
6
6
  "registry": "https://registry.npmjs.org/"
@@ -12,9 +12,7 @@
12
12
  "module": "dist/esm/index.js",
13
13
  "module:es2019": "dist/es2019/index.js",
14
14
  "types": "dist/types/index.d.ts",
15
- "sideEffects": [
16
- "*.compiled.css"
17
- ],
15
+ "sideEffects": ["*.compiled.css"],
18
16
  "atlaskit:src": "src/index.ts",
19
17
  "atlassian": {
20
18
  "react-compiler": {
@@ -41,20 +39,20 @@
41
39
  "@atlaskit/icon-lab": "^7.3.0",
42
40
  "@atlaskit/media-client": "^37.2.0",
43
41
  "@atlaskit/media-client-react": "^6.1.0",
44
- "@atlaskit/media-common": "^14.2.0",
42
+ "@atlaskit/media-common": "^14.3.0",
45
43
  "@atlaskit/media-document-viewer": "^1.1.0",
46
44
  "@atlaskit/media-state": "^3.0.0",
47
45
  "@atlaskit/media-svg": "^3.1.0",
48
- "@atlaskit/media-ui": "^30.5.0",
46
+ "@atlaskit/media-ui": "^30.6.0",
49
47
  "@atlaskit/platform-feature-flags": "^2.0.0",
50
48
  "@atlaskit/portal": "^6.1.0",
51
- "@atlaskit/primitives": "^20.3.0",
49
+ "@atlaskit/primitives": "^20.4.0",
52
50
  "@atlaskit/react-compiler-gating": "^0.2.0",
53
51
  "@atlaskit/side-navigation": "^12.2.0",
54
52
  "@atlaskit/spinner": "^20.1.0",
55
53
  "@atlaskit/textfield": "^9.1.0",
56
54
  "@atlaskit/theme": "^26.1.0",
57
- "@atlaskit/tmp-editor-statsig": "^121.0.0",
55
+ "@atlaskit/tmp-editor-statsig": "^121.3.0",
58
56
  "@atlaskit/tokens": "^15.4.0",
59
57
  "@atlaskit/tooltip": "^23.1.0",
60
58
  "@atlaskit/ufo": "^1.0.0",
@@ -89,12 +87,12 @@
89
87
  "@atlaskit/media-core": "^38.0.0",
90
88
  "@atlaskit/media-integration-test-helpers": "workspace:^",
91
89
  "@atlaskit/media-test-data": "^4.0.0",
92
- "@atlaskit/media-test-helpers": "^42.1.0",
90
+ "@atlaskit/media-test-helpers": "^42.2.0",
93
91
  "@atlaskit/ssr": "workspace:^",
94
92
  "@atlaskit/toggle": "^17.1.0",
95
93
  "@atlassian/a11y-jest-testing": "^0.13.0",
96
94
  "@atlassian/feature-flags-test-utils": "^1.1.0",
97
- "@atlassian/testing-library": "^0.8.0",
95
+ "@atlassian/testing-library": "^0.10.0",
98
96
  "@atlassian/ufo": "^0.8.0",
99
97
  "@testing-library/dom": "^10.1.0",
100
98
  "@testing-library/react": "^16.3.0",
@@ -143,14 +141,14 @@
143
141
  },
144
142
  "platform_media_archive_zip_guard": {
145
143
  "type": "boolean"
144
+ },
145
+ "platform_media_unsupported_mime_routing": {
146
+ "type": "boolean"
146
147
  }
147
148
  },
148
149
  "techstack": {
149
150
  "@repo/internal": {
150
- "design-tokens": [
151
- "color",
152
- "spacing"
153
- ]
151
+ "design-tokens": ["color", "spacing"]
154
152
  }
155
153
  }
156
154
  }