index.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import styled from '@emotion/styled';
  2. import moment from 'moment-timezone';
  3. import DateTime from 'sentry/components/dateTime';
  4. import TimeSince from 'sentry/components/timeSince';
  5. import space from 'sentry/styles/space';
  6. import textStyles from 'sentry/styles/text';
  7. import {AvatarUser} from 'sentry/types';
  8. import {isRenderFunc} from 'sentry/utils/isRenderFunc';
  9. import ActivityAvatar from './avatar';
  10. import ActivityBubble, {ActivityBubbleProps} from './bubble';
  11. export type ActivityAuthorType = 'user' | 'system';
  12. type ChildFunction = () => React.ReactNode;
  13. interface ActivityItemProps {
  14. /**
  15. * Used to render an avatar for the author. Currently can be a user, otherwise
  16. * defaults as a "system" avatar (i.e. sentry)
  17. *
  18. * `user` is required if `type` is "user"
  19. */
  20. author?: {
  21. type: ActivityAuthorType;
  22. user?: AvatarUser;
  23. };
  24. // Size of the avatar.
  25. avatarSize?: number;
  26. bubbleProps?: ActivityBubbleProps;
  27. children?: React.ReactChild | ChildFunction;
  28. className?: string;
  29. /**
  30. * If supplied, will show the time that the activity started
  31. */
  32. date?: string | Date;
  33. /**
  34. * Can be a react node or a render function. render function will not include default wrapper
  35. */
  36. footer?: React.ReactNode | ChildFunction;
  37. /**
  38. * Can be a react node or a render function. render function will not include default wrapper
  39. */
  40. header?: React.ReactNode | ChildFunction;
  41. // Hides date in header
  42. hideDate?: boolean;
  43. /**
  44. * This is used to uniquely identify the activity item for use as an anchor
  45. */
  46. id?: string;
  47. /**
  48. * If supplied, will show the interval that the activity occurred in
  49. */
  50. interval?: number;
  51. // Instead of showing a relative time/date, show the time
  52. showTime?: boolean;
  53. }
  54. function ActivityItem({
  55. author,
  56. avatarSize,
  57. bubbleProps,
  58. className,
  59. children,
  60. date,
  61. interval,
  62. footer,
  63. id,
  64. header,
  65. hideDate = false,
  66. showTime = false,
  67. }: ActivityItemProps) {
  68. const showDate = !hideDate && date && !interval;
  69. const showRange = !hideDate && date && interval;
  70. const dateEnded = showRange
  71. ? moment(date).add(interval, 'minutes').utc().format()
  72. : undefined;
  73. const timeOnly = Boolean(
  74. date && dateEnded && moment(date).date() === moment(dateEnded).date()
  75. );
  76. return (
  77. <ActivityItemWrapper data-test-id="activity-item" className={className}>
  78. {id && <a id={id} />}
  79. {author && (
  80. <StyledActivityAvatar type={author.type} user={author.user} size={avatarSize} />
  81. )}
  82. <StyledActivityBubble {...bubbleProps}>
  83. {header && isRenderFunc<ChildFunction>(header) && header()}
  84. {header && !isRenderFunc<ChildFunction>(header) && (
  85. <ActivityHeader>
  86. <ActivityHeaderContent>{header}</ActivityHeaderContent>
  87. {date && showDate && !showTime && <StyledTimeSince date={date} />}
  88. {date && showDate && showTime && <StyledDateTime timeOnly date={date} />}
  89. {showRange && (
  90. <StyledDateTimeWindow>
  91. <StyledDateTime timeOnly={timeOnly} date={date} />
  92. {' — '}
  93. <StyledDateTime timeOnly={timeOnly} date={dateEnded} />
  94. </StyledDateTimeWindow>
  95. )}
  96. </ActivityHeader>
  97. )}
  98. {children && isRenderFunc<ChildFunction>(children) && children()}
  99. {children && !isRenderFunc<ChildFunction>(children) && (
  100. <ActivityBody>{children}</ActivityBody>
  101. )}
  102. {footer && isRenderFunc<ChildFunction>(footer) && footer()}
  103. {footer && !isRenderFunc<ChildFunction>(footer) && (
  104. <ActivityFooter>{footer}</ActivityFooter>
  105. )}
  106. </StyledActivityBubble>
  107. </ActivityItemWrapper>
  108. );
  109. }
  110. const ActivityItemWrapper = styled('div')`
  111. display: flex;
  112. margin-bottom: ${space(2)};
  113. `;
  114. const HeaderAndFooter = styled('div')`
  115. padding: 6px ${space(2)};
  116. `;
  117. const ActivityHeader = styled(HeaderAndFooter)`
  118. display: flex;
  119. border-bottom: 1px solid ${p => p.theme.border};
  120. font-size: ${p => p.theme.fontSizeMedium};
  121. &:last-child {
  122. border-bottom: none;
  123. }
  124. `;
  125. const ActivityHeaderContent = styled('div')`
  126. flex: 1;
  127. `;
  128. const ActivityFooter = styled(HeaderAndFooter)`
  129. display: flex;
  130. border-top: 1px solid ${p => p.theme.border};
  131. font-size: ${p => p.theme.fontSizeMedium};
  132. `;
  133. const ActivityBody = styled('div')`
  134. padding: ${space(2)} ${space(2)};
  135. ${textStyles}
  136. `;
  137. const StyledActivityAvatar = styled(ActivityAvatar)`
  138. margin-right: ${space(1)};
  139. `;
  140. const StyledTimeSince = styled(TimeSince)`
  141. color: ${p => p.theme.gray300};
  142. `;
  143. const StyledDateTime = styled(DateTime)`
  144. color: ${p => p.theme.gray300};
  145. `;
  146. const StyledDateTimeWindow = styled('div')`
  147. color: ${p => p.theme.gray300};
  148. `;
  149. const StyledActivityBubble = styled(ActivityBubble)`
  150. width: 75%;
  151. overflow-wrap: break-word;
  152. `;
  153. export default ActivityItem;